@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/store.js CHANGED
@@ -1,6 +1,7 @@
1
- import v4_default from "./node_modules/uuid/dist/esm-node/v4.js";
1
+ import { escapeRediSearchTagValue } from "./utils.js";
2
2
  import { InvalidNamespaceError } from "@langchain/langgraph-checkpoint";
3
3
  import { createClient, createCluster } from "redis";
4
+ import { v4 } from "uuid";
4
5
 
5
6
  //#region src/store.ts
6
7
  function isPutOperation(op) {
@@ -104,7 +105,7 @@ var FilterBuilder = class {
104
105
  const keys = path.split(".");
105
106
  let current = obj;
106
107
  for (const key of keys) {
107
- if (current === null || current === void 0) return void 0;
108
+ if (current === null || current === void 0) return;
108
109
  current = current[key];
109
110
  }
110
111
  return current;
@@ -247,10 +248,7 @@ var RedisStore = class RedisStore {
247
248
  const prefixQuery = tokens.length > 0 ? `@prefix:(${tokens.join(" ")})` : "*";
248
249
  let query;
249
250
  if (key === "") query = prefixQuery;
250
- else {
251
- const escapedKey = this.escapeTagValue(key);
252
- query = `(${prefixQuery}) (@key:{${escapedKey}})`;
253
- }
251
+ else query = `(${prefixQuery}) (@key:{${this.escapeTagValue(key)}})`;
254
252
  try {
255
253
  const results = await this.client.ft.search("store", query, { LIMIT: {
256
254
  from: 0,
@@ -258,17 +256,17 @@ var RedisStore = class RedisStore {
258
256
  } });
259
257
  if (!results || !results.documents || results.documents.length === 0) return null;
260
258
  if (key === "") {
261
- for (const doc$1 of results.documents) {
262
- const jsonDoc$1 = doc$1.value;
263
- if (jsonDoc$1.key === "" && jsonDoc$1.prefix === prefix) {
264
- const docId$1 = doc$1.id;
265
- if (options?.refreshTTL) await this.refreshItemTTL(docId$1);
259
+ for (const doc of results.documents) {
260
+ const jsonDoc = doc.value;
261
+ if (jsonDoc.key === "" && jsonDoc.prefix === prefix) {
262
+ const docId = doc.id;
263
+ if (options?.refreshTTL) await this.refreshItemTTL(docId);
266
264
  return {
267
- value: jsonDoc$1.value,
268
- key: jsonDoc$1.key,
269
- namespace: jsonDoc$1.prefix.split("."),
270
- created_at: /* @__PURE__ */ new Date(jsonDoc$1.created_at / 1e6),
271
- updated_at: /* @__PURE__ */ new Date(jsonDoc$1.updated_at / 1e6)
265
+ value: jsonDoc.value,
266
+ key: jsonDoc.key,
267
+ namespace: jsonDoc.prefix.split("."),
268
+ created_at: /* @__PURE__ */ new Date(jsonDoc.created_at / 1e6),
269
+ updated_at: /* @__PURE__ */ new Date(jsonDoc.updated_at / 1e6)
272
270
  };
273
271
  }
274
272
  }
@@ -293,13 +291,11 @@ var RedisStore = class RedisStore {
293
291
  async put(namespace, key, value, options) {
294
292
  this.validateNamespace(namespace);
295
293
  const prefix = namespace.join(".");
296
- const docId = v4_default();
294
+ const docId = v4();
297
295
  const now = Date.now() * 1e6 + Math.floor(performance.now() * 1e3);
298
296
  let createdAt = now;
299
297
  const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
300
- const prefixQuery = tokens.length > 0 ? `@prefix:(${tokens.join(" ")})` : "*";
301
- const escapedKey = this.escapeTagValue(key);
302
- const existingQuery = `(${prefixQuery}) (@key:{${escapedKey}})`;
298
+ const existingQuery = `(${tokens.length > 0 ? `@prefix:(${tokens.join(" ")})` : "*"}) (@key:{${this.escapeTagValue(key)}})`;
303
299
  try {
304
300
  const existing = await this.client.ft.search("store", existingQuery, { LIMIT: {
305
301
  from: 0,
@@ -311,8 +307,7 @@ var RedisStore = class RedisStore {
311
307
  if (existingDoc && typeof existingDoc === "object" && "created_at" in existingDoc) createdAt = existingDoc.created_at;
312
308
  await this.client.del(oldDocId);
313
309
  if (this.indexConfig) {
314
- const oldUuid = oldDocId.split(":").pop();
315
- const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldUuid}`;
310
+ const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldDocId.split(":").pop()}`;
316
311
  try {
317
312
  await this.client.del(oldVectorKey);
318
313
  } catch (error) {}
@@ -350,9 +345,9 @@ var RedisStore = class RedisStore {
350
345
  updated_at: now
351
346
  };
352
347
  await this.client.json.set(vectorKey, "$", vectorDoc);
353
- const ttlMinutes$1 = options?.ttl || this.ttlConfig?.defaultTTL;
354
- if (ttlMinutes$1) {
355
- const ttlSeconds = Math.floor(ttlMinutes$1 * 60);
348
+ const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;
349
+ if (ttlMinutes) {
350
+ const ttlSeconds = Math.floor(ttlMinutes * 60);
356
351
  await this.client.expire(vectorKey, ttlSeconds);
357
352
  }
358
353
  }
@@ -373,10 +368,10 @@ var RedisStore = class RedisStore {
373
368
  const offset = options?.offset || 0;
374
369
  if (options?.query && this.indexConfig && this.embeddings) {
375
370
  const [embedding] = await this.embeddings.embedDocuments([options.query]);
376
- let queryStr$1 = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : "*";
371
+ let queryStr = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : "*";
377
372
  const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);
378
373
  try {
379
- const results = await this.client.ft.search("store_vectors", `(${queryStr$1})=>[KNN ${limit} @embedding $BLOB]`, {
374
+ const results = await this.client.ft.search("store_vectors", `(${queryStr})=>[KNN ${limit} @embedding $BLOB]`, {
380
375
  PARAMS: { BLOB: vectorBytes },
381
376
  DIALECT: 2,
382
377
  LIMIT: {
@@ -391,8 +386,7 @@ var RedisStore = class RedisStore {
391
386
  });
392
387
  const items = [];
393
388
  for (const doc of results.documents) {
394
- const docUuid = doc.id.split(":").pop();
395
- const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;
389
+ const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${doc.id.split(":").pop()}`;
396
390
  const storeDoc = await this.client.json.get(storeKey);
397
391
  if (storeDoc) {
398
392
  if (options.filter) {
@@ -553,20 +547,17 @@ var RedisStore = class RedisStore {
553
547
  namespaceCount: 0
554
548
  };
555
549
  try {
556
- const countResult = await this.client.ft.search("store", "*", { LIMIT: {
550
+ stats.totalDocuments = (await this.client.ft.search("store", "*", { LIMIT: {
557
551
  from: 0,
558
552
  size: 0
559
- } });
560
- stats.totalDocuments = countResult.total || 0;
561
- const namespaces = await this.listNamespaces({ limit: 1e3 });
562
- stats.namespaceCount = namespaces.length;
553
+ } })).total || 0;
554
+ stats.namespaceCount = (await this.listNamespaces({ limit: 1e3 })).length;
563
555
  if (this.indexConfig) {
564
556
  try {
565
- const vectorResult = await this.client.ft.search("store_vectors", "*", { LIMIT: {
557
+ stats.vectorDocuments = (await this.client.ft.search("store_vectors", "*", { LIMIT: {
566
558
  from: 0,
567
559
  size: 0
568
- } });
569
- stats.vectorDocuments = vectorResult.total || 0;
560
+ } })).total || 0;
570
561
  } catch (error) {
571
562
  stats.vectorDocuments = 0;
572
563
  }
@@ -592,24 +583,21 @@ var RedisStore = class RedisStore {
592
583
  if (this.ttlConfig?.defaultTTL) {
593
584
  const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
594
585
  await this.client.expire(docId, ttlSeconds);
595
- const docUuid = docId.split(":").pop();
596
- const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;
586
+ const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docId.split(":").pop()}`;
597
587
  try {
598
588
  await this.client.expire(vectorKey, ttlSeconds);
599
589
  } catch (error) {}
600
590
  }
601
591
  }
602
592
  escapeTagValue(value) {
603
- if (value === "") return "__EMPTY_STRING__";
604
- return value.replace(/\\/g, "\\\\").replace(/[-\s,.:<>{}[\]"';!@#$%^&*()+=~|?/]/g, "\\$&");
593
+ return escapeRediSearchTagValue(value);
605
594
  }
606
595
  /**
607
596
  * Calculate similarity score based on the distance metric.
608
597
  * Converts raw distance to a normalized similarity score [0,1].
609
598
  */
610
599
  calculateSimilarityScore(distance) {
611
- const metric = this.indexConfig?.distanceType || "cosine";
612
- switch (metric) {
600
+ switch (this.indexConfig?.distanceType || "cosine") {
613
601
  case "cosine": return Math.max(0, 1 - distance / 2);
614
602
  case "l2": return Math.exp(-distance);
615
603
  case "ip": return 1 / (1 + Math.exp(-distance));
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","names":["queryParts: string[]","error: any","vectorSchema: Record<string, any>","query: string","doc","jsonDoc","docId","uuidv4","vectorDoc: VectorDocument","ttlMinutes","queryStr","items: SearchItem[]","results: any[]","prefix: string[] | undefined","suffix: string[] | undefined","stats: {\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n }"],"sources":["../src/store.ts"],"sourcesContent":["import { createClient, createCluster } from \"redis\";\n\n/** A conventional Redis connection. */\nexport type RedisClientConnection = ReturnType<typeof createClient>;\n\n/** A clustered Redis connection. */\nexport type RedisClusterConnection = ReturnType<typeof createCluster>;\n\n/** A Redis connection, clustered or conventional. */\nexport type RedisConnection = RedisClientConnection | RedisClusterConnection;\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n type GetOperation,\n InvalidNamespaceError,\n type ListNamespacesOperation,\n type Operation,\n type PutOperation,\n type SearchOperation,\n} from \"@langchain/langgraph-checkpoint\";\n\n// Type guard functions for operations\nexport function isPutOperation(op: Operation): op is PutOperation {\n return \"value\" in op && \"namespace\" in op && \"key\" in op;\n}\n\nexport function isGetOperation(op: Operation): op is GetOperation {\n return (\n \"namespace\" in op &&\n \"key\" in op &&\n !(\"value\" in op) &&\n !(\"namespacePrefix\" in op) &&\n !(\"matchConditions\" in op)\n );\n}\n\nexport function isSearchOperation(op: Operation): op is SearchOperation {\n return \"namespacePrefix\" in op;\n}\n\nexport function isListNamespacesOperation(\n op: Operation\n): op is ListNamespacesOperation {\n return \"matchConditions\" in op;\n}\n\n// Filter types for advanced search operations\nexport interface FilterOperators {\n $eq?: any;\n $ne?: any;\n $gt?: number;\n $gte?: number;\n $lt?: number;\n $lte?: number;\n $in?: any[];\n $nin?: any[];\n $exists?: boolean;\n}\n\nexport type FilterValue = any | FilterOperators;\nexport type Filter = Record<string, FilterValue>;\n\n/**\n * Internal class for evaluating filters against documents.\n * Supports MongoDB-style query operators.\n */\nclass FilterBuilder {\n /**\n * Evaluates if a document matches the given filter criteria.\n * Supports advanced operators like $gt, $lt, $in, etc.\n */\n static matchesFilter(doc: Record<string, any>, filter: Filter): boolean {\n for (const [key, filterValue] of Object.entries(filter)) {\n if (!this.matchesFieldFilter(doc, key, filterValue)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Builds a Redis Search query string from filter criteria.\n * Note: This is limited by RediSearch capabilities and may not support all operators.\n */\n static buildRedisSearchQuery(\n filter: Filter,\n prefix?: string\n ): { query: string; useClientFilter: boolean } {\n let queryParts: string[] = [];\n let useClientFilter = false;\n\n // Add prefix filter if provided\n if (prefix) {\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n queryParts.push(`@prefix:(${tokens.join(\" \")})`);\n }\n }\n\n // Check if we have complex operators that require client-side filtering\n for (const [_key, value] of Object.entries(filter)) {\n if (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.keys(value).some((k) => k.startsWith(\"$\"))\n ) {\n // Complex operators require client-side filtering\n useClientFilter = true;\n break;\n }\n }\n\n // If no prefix, at least search all documents\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n return {\n query: queryParts.join(\" \"),\n useClientFilter,\n };\n }\n\n private static matchesFieldFilter(\n doc: Record<string, any>,\n key: string,\n filterValue: FilterValue\n ): boolean {\n // Handle nested keys (e.g., \"user.name\")\n const actualValue = this.getNestedValue(doc, key);\n\n // Check if it's an operator object\n if (\n typeof filterValue === \"object\" &&\n filterValue !== null &&\n !Array.isArray(filterValue) &&\n Object.keys(filterValue).some((k) => k.startsWith(\"$\"))\n ) {\n // Handle operator object\n return this.matchesOperators(actualValue, filterValue as FilterOperators);\n } else {\n // Simple equality check\n return this.isEqual(actualValue, filterValue);\n }\n }\n\n private static matchesOperators(\n actualValue: any,\n operators: FilterOperators\n ): boolean {\n for (const [operator, operatorValue] of Object.entries(operators)) {\n if (!this.matchesOperator(actualValue, operator, operatorValue)) {\n return false;\n }\n }\n return true;\n }\n\n private static matchesOperator(\n actualValue: any,\n operator: string,\n operatorValue: any\n ): boolean {\n switch (operator) {\n case \"$eq\":\n return this.isEqual(actualValue, operatorValue);\n\n case \"$ne\":\n return !this.isEqual(actualValue, operatorValue);\n\n case \"$gt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) > Number(operatorValue)\n );\n\n case \"$gte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) >= Number(operatorValue)\n );\n\n case \"$lt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) < Number(operatorValue)\n );\n\n case \"$lte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) <= Number(operatorValue)\n );\n\n case \"$in\":\n if (!Array.isArray(operatorValue)) return false;\n return operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$nin\":\n if (!Array.isArray(operatorValue)) return false;\n return !operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$exists\": {\n const exists = actualValue !== undefined;\n return operatorValue ? exists : !exists;\n }\n\n default:\n // Unknown operator, return false for safety\n return false;\n }\n }\n\n private static isEqual(a: any, b: any): boolean {\n // Handle null and undefined\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a === undefined || b === undefined) return false;\n\n // Handle arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((val, idx) => this.isEqual(val, b[idx]));\n }\n if (Array.isArray(a) || Array.isArray(b)) {\n // Check if non-array value exists in array\n const arr = Array.isArray(a) ? a : b;\n const val = Array.isArray(a) ? b : a;\n return arr.includes(val);\n }\n\n // Handle objects\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) => this.isEqual(a[key], b[key]));\n }\n\n // Primitive comparison (with type coercion for numbers)\n return a == b;\n }\n\n private static getNestedValue(obj: any, path: string): any {\n const keys = path.split(\".\");\n let current = obj;\n\n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n current = current[key];\n }\n\n return current;\n }\n}\n\nexport interface Item {\n value: any;\n key: string;\n namespace: string[];\n created_at: Date;\n updated_at: Date;\n}\n\nexport interface SearchItem extends Item {\n score?: number;\n}\n\ninterface StoreDocument {\n key: string;\n prefix: string;\n value: any;\n created_at: number;\n updated_at: number;\n}\n\ninterface VectorDocument {\n prefix: string;\n key: string;\n field_name: string;\n embedding: number[];\n created_at: number;\n updated_at: number;\n}\n\nexport interface IndexConfig {\n dims: number;\n embed?: any;\n distanceType?: \"cosine\" | \"l2\" | \"ip\"; // cosine, L2 (Euclidean), inner product\n fields?: string[];\n vectorStorageType?: string;\n similarityThreshold?: number; // Minimum similarity score for results\n}\n\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n\nexport interface StoreConfig {\n index?: IndexConfig;\n ttl?: TTLConfig;\n}\n\nconst REDIS_KEY_SEPARATOR = \":\";\nconst STORE_PREFIX = \"store\";\nconst STORE_VECTOR_PREFIX = \"store_vectors\";\n\nconst SCHEMAS = [\n {\n index: \"store\",\n prefix: STORE_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n {\n index: \"store_vectors\",\n prefix: STORE_VECTOR_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.embedding\": { type: \"VECTOR\", AS: \"embedding\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n];\n\nexport class RedisStore {\n private readonly client: RedisConnection;\n private readonly indexConfig?: IndexConfig;\n private readonly ttlConfig?: TTLConfig;\n private readonly embeddings?: any;\n\n constructor(client: RedisConnection, config?: StoreConfig) {\n this.client = client;\n this.indexConfig = config?.index;\n this.ttlConfig = config?.ttl;\n\n if (this.indexConfig?.embed) {\n this.embeddings = this.indexConfig.embed;\n }\n }\n\n static async fromConnString(\n connString: string,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createClient({ url: connString });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n async setup(): Promise<void> {\n // Create store index\n try {\n await this.client.ft.create(SCHEMAS[0].index, SCHEMAS[0].schema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[0].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create store index:\", error.message);\n }\n }\n\n // Create vector index if configured\n if (this.indexConfig) {\n const dims = this.indexConfig.dims;\n const distanceMetric =\n this.indexConfig.distanceType === \"cosine\"\n ? \"COSINE\"\n : this.indexConfig.distanceType === \"l2\"\n ? \"L2\"\n : this.indexConfig.distanceType === \"ip\"\n ? \"IP\"\n : \"COSINE\";\n\n // Build schema with correct vector syntax\n const vectorSchema: Record<string, any> = {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n };\n\n // Add vector field with correct syntax\n vectorSchema[\"$.embedding\"] = {\n type: \"VECTOR\",\n ALGORITHM: \"FLAT\",\n TYPE: \"FLOAT32\",\n DIM: dims,\n DISTANCE_METRIC: distanceMetric,\n AS: \"embedding\",\n };\n\n try {\n await this.client.ft.create(SCHEMAS[1].index, vectorSchema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[1].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create vector index:\", error.message);\n }\n }\n }\n }\n\n async get(\n namespace: string[],\n key: string,\n options?: { refreshTTL?: boolean }\n ): Promise<Item | null> {\n const prefix = namespace.join(\".\");\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n // Handle empty string as a special case\n let query: string;\n if (key === \"\") {\n // For empty keys, search by prefix and filter results\n query = prefixQuery;\n } else {\n const escapedKey = this.escapeTagValue(key);\n query = `(${prefixQuery}) (@key:{${escapedKey}})`;\n }\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: key === \"\" ? 100 : 1 },\n });\n\n if (!results || !results.documents || results.documents.length === 0) {\n return null;\n }\n\n // For empty key, filter to find the exact match\n if (key === \"\") {\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n if (jsonDoc.key === \"\" && jsonDoc.prefix === prefix) {\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n }\n }\n return null;\n }\n\n const doc = results.documents[0];\n const jsonDoc = doc.value as unknown as StoreDocument;\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return null;\n }\n throw error;\n }\n }\n\n async put(\n namespace: string[],\n key: string,\n value: any,\n options?: { ttl?: number; index?: boolean | string[] }\n ): Promise<void> {\n // Validate namespace for put operations\n this.validateNamespace(namespace);\n const prefix = namespace.join(\".\");\n const docId = uuidv4();\n // Use high-resolution time for better timestamp precision\n const now = Date.now() * 1000000 + Math.floor(performance.now() * 1000); // Microseconds + nanoseconds component\n let createdAt = now; // Will be overridden if document exists\n\n // Delete existing document if it exists\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n const escapedKey = this.escapeTagValue(key);\n const existingQuery = `(${prefixQuery}) (@key:{${escapedKey}})`;\n try {\n const existing = await this.client.ft.search(\"store\", existingQuery, {\n LIMIT: { from: 0, size: 1 },\n });\n\n if (existing && existing.documents && existing.documents.length > 0) {\n const oldDocId = existing.documents[0].id;\n // Preserve the original created_at timestamp\n const existingDoc = await this.client.json.get(oldDocId);\n if (\n existingDoc &&\n typeof existingDoc === \"object\" &&\n \"created_at\" in existingDoc\n ) {\n createdAt = (existingDoc as any).created_at;\n }\n await this.client.del(oldDocId);\n\n // Also delete associated vector if it exists\n if (this.indexConfig) {\n const oldUuid = oldDocId.split(\":\").pop();\n const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldUuid}`;\n try {\n await this.client.del(oldVectorKey);\n } catch (error) {\n // Vector might not exist\n }\n }\n }\n } catch (error) {\n // Index might not exist yet\n }\n\n // Handle delete operation\n if (value === null) {\n return;\n }\n\n // Store the document\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const doc = {\n prefix,\n key,\n value,\n created_at: createdAt,\n updated_at: now,\n };\n\n await this.client.json.set(storeKey, \"$\", doc);\n\n // Handle embeddings if configured\n if (this.indexConfig && this.embeddings && options?.index !== false) {\n const fieldsToIndex =\n options && Array.isArray(options.index)\n ? options.index\n : this.indexConfig.fields || [\"text\"];\n const textsToEmbed = [];\n const fieldNames = [];\n\n for (const field of fieldsToIndex) {\n if (value[field]) {\n textsToEmbed.push(value[field]);\n fieldNames.push(field);\n }\n }\n\n if (textsToEmbed.length > 0) {\n const embeddings = await this.embeddings.embedDocuments(textsToEmbed);\n\n for (let i = 0; i < embeddings.length; i++) {\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const vectorDoc: VectorDocument = {\n prefix,\n key,\n field_name: fieldNames[i],\n embedding: embeddings[i],\n created_at: now,\n updated_at: now,\n };\n\n await this.client.json.set(vectorKey, \"$\", vectorDoc as any);\n\n // Apply TTL to vector key if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(vectorKey, ttlSeconds);\n }\n }\n }\n }\n\n // Apply TTL if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(storeKey, ttlSeconds);\n }\n }\n\n async delete(namespace: string[], key: string): Promise<void> {\n await this.put(namespace, key, null);\n }\n\n async search(\n namespacePrefix: string[],\n options?: {\n filter?: Filter;\n query?: string;\n limit?: number;\n offset?: number;\n refreshTTL?: boolean;\n similarityThreshold?: number;\n }\n ): Promise<SearchItem[]> {\n const prefix = namespacePrefix.join(\".\");\n const limit = options?.limit || 10;\n const offset = options?.offset || 0;\n\n // Handle vector search if query is provided\n if (options?.query && this.indexConfig && this.embeddings) {\n const [embedding] = await this.embeddings.embedDocuments([options.query]);\n\n // Build KNN query\n // For prefix search, use wildcard since we want to match any document starting with this prefix\n let queryStr = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : \"*\";\n const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);\n\n try {\n // Use KNN query with proper syntax\n const results = await this.client.ft.search(\n \"store_vectors\",\n `(${queryStr})=>[KNN ${limit} @embedding $BLOB]`,\n {\n PARAMS: {\n BLOB: vectorBytes,\n },\n DIALECT: 2,\n LIMIT: { from: offset, size: limit },\n RETURN: [\"prefix\", \"key\", \"__embedding_score\"],\n }\n );\n\n // Get matching store documents\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const docUuid = doc.id.split(\":\").pop();\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n\n const storeDoc = (await this.client.json.get(\n storeKey\n )) as StoreDocument | null;\n if (storeDoc) {\n // Apply advanced filter if provided\n if (options.filter) {\n if (\n !FilterBuilder.matchesFilter(\n storeDoc.value || {},\n options.filter\n )\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options.refreshTTL) {\n await this.refreshItemTTL(storeKey);\n await this.refreshItemTTL(doc.id);\n }\n\n const score = (doc.value as any)?.__embedding_score\n ? this.calculateSimilarityScore(\n parseFloat((doc.value as any).__embedding_score as string)\n )\n : 0;\n\n // Apply similarity threshold if specified\n const threshold =\n options.similarityThreshold ??\n this.indexConfig?.similarityThreshold;\n if (threshold !== undefined && score < threshold) {\n continue;\n }\n\n items.push({\n value: storeDoc.value,\n key: storeDoc.key,\n namespace: storeDoc.prefix.split(\".\"),\n created_at: new Date(storeDoc.created_at / 1000000),\n updated_at: new Date(storeDoc.updated_at / 1000000),\n score,\n });\n }\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n // Regular search without vectors\n let queryStr = \"*\";\n if (prefix) {\n // For prefix search, we need to match all tokens from the namespace prefix\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n // Match all tokens to ensure we get the right prefix\n queryStr = `@prefix:(${tokens.join(\" \")})`;\n }\n }\n\n try {\n const results = await this.client.ft.search(\"store\", queryStr, {\n LIMIT: { from: offset, size: limit },\n SORTBY: { BY: \"created_at\", DIRECTION: \"DESC\" },\n });\n\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n\n // Apply advanced filter\n if (options?.filter) {\n if (\n !FilterBuilder.matchesFilter(jsonDoc.value || {}, options.filter)\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(doc.id);\n }\n\n items.push({\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n });\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async listNamespaces(options?: {\n prefix?: string[];\n suffix?: string[];\n maxDepth?: number;\n limit?: number;\n offset?: number;\n }): Promise<string[][]> {\n let query = \"*\";\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: 1000 }, // Get many to deduplicate\n RETURN: [\"prefix\"],\n });\n\n // Extract unique namespaces and filter\n const namespaceSet = new Set<string>();\n for (const doc of results.documents) {\n const prefix = (doc.value as unknown as StoreDocument).prefix;\n const parts = prefix.split(\".\");\n\n // Apply prefix filter if specified\n if (options?.prefix) {\n // Check if this namespace starts with the specified prefix\n if (parts.length < options.prefix.length) continue;\n\n let matches = true;\n for (let i = 0; i < options.prefix.length; i++) {\n if (parts[i] !== options.prefix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply suffix filter if specified\n if (options?.suffix) {\n // Check if this namespace ends with the specified suffix\n if (parts.length < options.suffix.length) continue;\n\n let matches = true;\n const startIdx = parts.length - options.suffix.length;\n for (let i = 0; i < options.suffix.length; i++) {\n if (parts[startIdx + i] !== options.suffix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply max depth\n if (options?.maxDepth) {\n const truncated = parts.slice(0, options.maxDepth);\n namespaceSet.add(truncated.join(\".\"));\n } else {\n namespaceSet.add(prefix);\n }\n }\n\n // Convert to array of arrays and sort\n let namespaces = Array.from(namespaceSet)\n .map((ns) => ns.split(\".\"))\n .sort((a, b) => a.join(\".\").localeCompare(b.join(\".\")));\n\n // Apply pagination\n if (options?.offset || options?.limit) {\n const offset = options.offset || 0;\n const limit = options.limit || 10;\n namespaces = namespaces.slice(offset, offset + limit);\n }\n\n return namespaces;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async batch(ops: Operation[]): Promise<any[]> {\n const results: any[] = new Array(ops.length).fill(null);\n\n // Process operations in order to maintain dependencies\n for (let idx = 0; idx < ops.length; idx++) {\n const op = ops[idx];\n\n // Execute operation based on type guards\n if (isPutOperation(op)) {\n // TypeScript now knows op is PutOperation\n await this.put(op.namespace, op.key, op.value);\n results[idx] = null;\n } else if (isSearchOperation(op)) {\n // TypeScript now knows op is SearchOperation\n results[idx] = await this.search(op.namespacePrefix, {\n filter: op.filter,\n query: op.query,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isListNamespacesOperation(op)) {\n // TypeScript now knows op is ListNamespacesOperation\n let prefix: string[] | undefined = undefined;\n let suffix: string[] | undefined = undefined;\n\n if (op.matchConditions) {\n for (const condition of op.matchConditions) {\n if (condition.matchType === \"prefix\") {\n prefix = condition.path;\n } else if (condition.matchType === \"suffix\") {\n suffix = condition.path;\n }\n }\n }\n\n results[idx] = await this.listNamespaces({\n prefix,\n suffix,\n maxDepth: op.maxDepth,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isGetOperation(op)) {\n // TypeScript now knows op is GetOperation\n results[idx] = await this.get(op.namespace, op.key);\n } else {\n // This should never happen with proper Operation type\n throw new Error(`Unknown operation type: ${JSON.stringify(op)}`);\n }\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Get statistics about the store.\n * Returns document counts and other metrics.\n */\n async getStatistics(): Promise<{\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n }> {\n const stats: {\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n } = {\n totalDocuments: 0,\n namespaceCount: 0,\n };\n\n try {\n // Get total document count\n const countResult = await this.client.ft.search(\"store\", \"*\", {\n LIMIT: { from: 0, size: 0 },\n });\n stats.totalDocuments = countResult.total || 0;\n\n // Get unique namespace count\n const namespaces = await this.listNamespaces({ limit: 1000 });\n stats.namespaceCount = namespaces.length;\n\n // Get vector document count if index is configured\n if (this.indexConfig) {\n try {\n const vectorResult = await this.client.ft.search(\n \"store_vectors\",\n \"*\",\n {\n LIMIT: { from: 0, size: 0 },\n }\n );\n stats.vectorDocuments = vectorResult.total || 0;\n } catch (error) {\n // Vector index might not exist\n stats.vectorDocuments = 0;\n }\n\n // Get index info\n try {\n stats.indexInfo = await this.client.ft.info(\"store\");\n } catch (error) {\n // Index info might not be available\n }\n }\n } catch (error: any) {\n if (!error.message?.includes(\"no such index\")) {\n throw error;\n }\n }\n\n return stats;\n }\n\n private validateNamespace(namespace: string[]): void {\n if (namespace.length === 0) {\n throw new InvalidNamespaceError(\"Namespace cannot be empty.\");\n }\n for (const label of namespace) {\n // Runtime check for JavaScript users (TypeScript already ensures this)\n // This check is for runtime safety when called from JavaScript\n // noinspection SuspiciousTypeOfGuard\n if (typeof label !== \"string\") {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${String(\n label\n )}' found in ${namespace}. Namespace labels must be strings.`\n );\n }\n if (label.includes(\".\")) {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${label}' found in ${namespace}. Namespace labels cannot contain periods ('.').`\n );\n }\n if (label === \"\") {\n throw new InvalidNamespaceError(\n `Namespace labels cannot be empty strings. Got ${label} in ${namespace}`\n );\n }\n }\n if (namespace[0] === \"langgraph\") {\n throw new InvalidNamespaceError(\n `Root label for namespace cannot be \"langgraph\". Got: ${namespace}`\n );\n }\n }\n\n private async refreshItemTTL(docId: string): Promise<void> {\n if (this.ttlConfig?.defaultTTL) {\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n await this.client.expire(docId, ttlSeconds);\n\n // Also refresh vector key if it exists\n const docUuid = docId.split(\":\").pop();\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n try {\n await this.client.expire(vectorKey, ttlSeconds);\n } catch (error) {\n // Vector key might not exist\n }\n }\n }\n\n private escapeTagValue(value: string): string {\n // For TAG fields, we need to escape special characters\n // Based on RediSearch documentation, these characters need escaping in TAG fields\n // when used within curly braces: , . < > { } [ ] \" ' : ; ! @ # $ % ^ & * ( ) - + = ~ | \\ ? /\n // Handle empty string as a special case - use a placeholder\n if (value === \"\") {\n // Use a special placeholder for empty strings\n return \"__EMPTY_STRING__\";\n }\n // We'll escape the most common ones that appear in keys\n return value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/[-\\s,.:<>{}[\\]\"';!@#$%^&*()+=~|?/]/g, \"\\\\$&\");\n }\n\n /**\n * Calculate similarity score based on the distance metric.\n * Converts raw distance to a normalized similarity score [0,1].\n */\n private calculateSimilarityScore(distance: number): number {\n const metric = this.indexConfig?.distanceType || \"cosine\";\n\n switch (metric) {\n case \"cosine\":\n // Cosine distance is in range [0,2], convert to similarity [0,1]\n return Math.max(0, 1 - distance / 2);\n\n case \"l2\":\n // L2 (Euclidean) distance, use exponential decay\n // Similarity = e^(-distance)\n return Math.exp(-distance);\n\n case \"ip\":\n // Inner product can be negative, use sigmoid function\n // Similarity = 1 / (1 + e^(-distance))\n return 1 / (1 + Math.exp(-distance));\n\n default:\n // Default to cosine similarity\n return Math.max(0, 1 - distance / 2);\n }\n }\n}\n\n// Export FilterBuilder for testing purposes\nexport { FilterBuilder };\n"],"mappings":"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"}
1
+ {"version":3,"file":"store.js","names":["uuidv4"],"sources":["../src/store.ts"],"sourcesContent":["import { createClient, createCluster } from \"redis\";\n\n/** A conventional Redis connection. */\nexport type RedisClientConnection = ReturnType<typeof createClient>;\n\n/** A clustered Redis connection. */\nexport type RedisClusterConnection = ReturnType<typeof createCluster>;\n\n/** A Redis connection, clustered or conventional. */\nexport type RedisConnection = RedisClientConnection | RedisClusterConnection;\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n type GetOperation,\n InvalidNamespaceError,\n type ListNamespacesOperation,\n type Operation,\n type PutOperation,\n type SearchOperation,\n} from \"@langchain/langgraph-checkpoint\";\n\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\n// Type guard functions for operations\nexport function isPutOperation(op: Operation): op is PutOperation {\n return \"value\" in op && \"namespace\" in op && \"key\" in op;\n}\n\nexport function isGetOperation(op: Operation): op is GetOperation {\n return (\n \"namespace\" in op &&\n \"key\" in op &&\n !(\"value\" in op) &&\n !(\"namespacePrefix\" in op) &&\n !(\"matchConditions\" in op)\n );\n}\n\nexport function isSearchOperation(op: Operation): op is SearchOperation {\n return \"namespacePrefix\" in op;\n}\n\nexport function isListNamespacesOperation(\n op: Operation\n): op is ListNamespacesOperation {\n return \"matchConditions\" in op;\n}\n\n// Filter types for advanced search operations\nexport interface FilterOperators {\n $eq?: any;\n $ne?: any;\n $gt?: number;\n $gte?: number;\n $lt?: number;\n $lte?: number;\n $in?: any[];\n $nin?: any[];\n $exists?: boolean;\n}\n\nexport type FilterValue = any | FilterOperators;\nexport type Filter = Record<string, FilterValue>;\n\n/**\n * Internal class for evaluating filters against documents.\n * Supports MongoDB-style query operators.\n */\nclass FilterBuilder {\n /**\n * Evaluates if a document matches the given filter criteria.\n * Supports advanced operators like $gt, $lt, $in, etc.\n */\n static matchesFilter(doc: Record<string, any>, filter: Filter): boolean {\n for (const [key, filterValue] of Object.entries(filter)) {\n if (!this.matchesFieldFilter(doc, key, filterValue)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Builds a Redis Search query string from filter criteria.\n * Note: This is limited by RediSearch capabilities and may not support all operators.\n */\n static buildRedisSearchQuery(\n filter: Filter,\n prefix?: string\n ): { query: string; useClientFilter: boolean } {\n let queryParts: string[] = [];\n let useClientFilter = false;\n\n // Add prefix filter if provided\n if (prefix) {\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n queryParts.push(`@prefix:(${tokens.join(\" \")})`);\n }\n }\n\n // Check if we have complex operators that require client-side filtering\n for (const [_key, value] of Object.entries(filter)) {\n if (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.keys(value).some((k) => k.startsWith(\"$\"))\n ) {\n // Complex operators require client-side filtering\n useClientFilter = true;\n break;\n }\n }\n\n // If no prefix, at least search all documents\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n return {\n query: queryParts.join(\" \"),\n useClientFilter,\n };\n }\n\n private static matchesFieldFilter(\n doc: Record<string, any>,\n key: string,\n filterValue: FilterValue\n ): boolean {\n // Handle nested keys (e.g., \"user.name\")\n const actualValue = this.getNestedValue(doc, key);\n\n // Check if it's an operator object\n if (\n typeof filterValue === \"object\" &&\n filterValue !== null &&\n !Array.isArray(filterValue) &&\n Object.keys(filterValue).some((k) => k.startsWith(\"$\"))\n ) {\n // Handle operator object\n return this.matchesOperators(actualValue, filterValue as FilterOperators);\n } else {\n // Simple equality check\n return this.isEqual(actualValue, filterValue);\n }\n }\n\n private static matchesOperators(\n actualValue: any,\n operators: FilterOperators\n ): boolean {\n for (const [operator, operatorValue] of Object.entries(operators)) {\n if (!this.matchesOperator(actualValue, operator, operatorValue)) {\n return false;\n }\n }\n return true;\n }\n\n private static matchesOperator(\n actualValue: any,\n operator: string,\n operatorValue: any\n ): boolean {\n switch (operator) {\n case \"$eq\":\n return this.isEqual(actualValue, operatorValue);\n\n case \"$ne\":\n return !this.isEqual(actualValue, operatorValue);\n\n case \"$gt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) > Number(operatorValue)\n );\n\n case \"$gte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) >= Number(operatorValue)\n );\n\n case \"$lt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) < Number(operatorValue)\n );\n\n case \"$lte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) <= Number(operatorValue)\n );\n\n case \"$in\":\n if (!Array.isArray(operatorValue)) return false;\n return operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$nin\":\n if (!Array.isArray(operatorValue)) return false;\n return !operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$exists\": {\n const exists = actualValue !== undefined;\n return operatorValue ? exists : !exists;\n }\n\n default:\n // Unknown operator, return false for safety\n return false;\n }\n }\n\n private static isEqual(a: any, b: any): boolean {\n // Handle null and undefined\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a === undefined || b === undefined) return false;\n\n // Handle arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((val, idx) => this.isEqual(val, b[idx]));\n }\n if (Array.isArray(a) || Array.isArray(b)) {\n // Check if non-array value exists in array\n const arr = Array.isArray(a) ? a : b;\n const val = Array.isArray(a) ? b : a;\n return arr.includes(val);\n }\n\n // Handle objects\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) => this.isEqual(a[key], b[key]));\n }\n\n // Primitive comparison (with type coercion for numbers)\n return a == b;\n }\n\n private static getNestedValue(obj: any, path: string): any {\n const keys = path.split(\".\");\n let current = obj;\n\n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n current = current[key];\n }\n\n return current;\n }\n}\n\nexport interface Item {\n value: any;\n key: string;\n namespace: string[];\n created_at: Date;\n updated_at: Date;\n}\n\nexport interface SearchItem extends Item {\n score?: number;\n}\n\ninterface StoreDocument {\n key: string;\n prefix: string;\n value: any;\n created_at: number;\n updated_at: number;\n}\n\ninterface VectorDocument {\n prefix: string;\n key: string;\n field_name: string;\n embedding: number[];\n created_at: number;\n updated_at: number;\n}\n\nexport interface IndexConfig {\n dims: number;\n embed?: any;\n distanceType?: \"cosine\" | \"l2\" | \"ip\"; // cosine, L2 (Euclidean), inner product\n fields?: string[];\n vectorStorageType?: string;\n similarityThreshold?: number; // Minimum similarity score for results\n}\n\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n\nexport interface StoreConfig {\n index?: IndexConfig;\n ttl?: TTLConfig;\n}\n\nconst REDIS_KEY_SEPARATOR = \":\";\nconst STORE_PREFIX = \"store\";\nconst STORE_VECTOR_PREFIX = \"store_vectors\";\n\nconst SCHEMAS = [\n {\n index: \"store\",\n prefix: STORE_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n {\n index: \"store_vectors\",\n prefix: STORE_VECTOR_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.embedding\": { type: \"VECTOR\", AS: \"embedding\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n];\n\nexport class RedisStore {\n private readonly client: RedisConnection;\n private readonly indexConfig?: IndexConfig;\n private readonly ttlConfig?: TTLConfig;\n private readonly embeddings?: any;\n\n constructor(client: RedisConnection, config?: StoreConfig) {\n this.client = client;\n this.indexConfig = config?.index;\n this.ttlConfig = config?.ttl;\n\n if (this.indexConfig?.embed) {\n this.embeddings = this.indexConfig.embed;\n }\n }\n\n static async fromConnString(\n connString: string,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createClient({ url: connString });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n async setup(): Promise<void> {\n // Create store index\n try {\n await this.client.ft.create(SCHEMAS[0].index, SCHEMAS[0].schema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[0].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create store index:\", error.message);\n }\n }\n\n // Create vector index if configured\n if (this.indexConfig) {\n const dims = this.indexConfig.dims;\n const distanceMetric =\n this.indexConfig.distanceType === \"cosine\"\n ? \"COSINE\"\n : this.indexConfig.distanceType === \"l2\"\n ? \"L2\"\n : this.indexConfig.distanceType === \"ip\"\n ? \"IP\"\n : \"COSINE\";\n\n // Build schema with correct vector syntax\n const vectorSchema: Record<string, any> = {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n };\n\n // Add vector field with correct syntax\n vectorSchema[\"$.embedding\"] = {\n type: \"VECTOR\",\n ALGORITHM: \"FLAT\",\n TYPE: \"FLOAT32\",\n DIM: dims,\n DISTANCE_METRIC: distanceMetric,\n AS: \"embedding\",\n };\n\n try {\n await this.client.ft.create(SCHEMAS[1].index, vectorSchema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[1].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create vector index:\", error.message);\n }\n }\n }\n }\n\n async get(\n namespace: string[],\n key: string,\n options?: { refreshTTL?: boolean }\n ): Promise<Item | null> {\n const prefix = namespace.join(\".\");\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n // Handle empty string as a special case\n let query: string;\n if (key === \"\") {\n // For empty keys, search by prefix and filter results\n query = prefixQuery;\n } else {\n const escapedKey = this.escapeTagValue(key);\n query = `(${prefixQuery}) (@key:{${escapedKey}})`;\n }\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: key === \"\" ? 100 : 1 },\n });\n\n if (!results || !results.documents || results.documents.length === 0) {\n return null;\n }\n\n // For empty key, filter to find the exact match\n if (key === \"\") {\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n if (jsonDoc.key === \"\" && jsonDoc.prefix === prefix) {\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n }\n }\n return null;\n }\n\n const doc = results.documents[0];\n const jsonDoc = doc.value as unknown as StoreDocument;\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return null;\n }\n throw error;\n }\n }\n\n async put(\n namespace: string[],\n key: string,\n value: any,\n options?: { ttl?: number; index?: boolean | string[] }\n ): Promise<void> {\n // Validate namespace for put operations\n this.validateNamespace(namespace);\n const prefix = namespace.join(\".\");\n const docId = uuidv4();\n // Use high-resolution time for better timestamp precision\n const now = Date.now() * 1000000 + Math.floor(performance.now() * 1000); // Microseconds + nanoseconds component\n let createdAt = now; // Will be overridden if document exists\n\n // Delete existing document if it exists\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n const escapedKey = this.escapeTagValue(key);\n const existingQuery = `(${prefixQuery}) (@key:{${escapedKey}})`;\n try {\n const existing = await this.client.ft.search(\"store\", existingQuery, {\n LIMIT: { from: 0, size: 1 },\n });\n\n if (existing && existing.documents && existing.documents.length > 0) {\n const oldDocId = existing.documents[0].id;\n // Preserve the original created_at timestamp\n const existingDoc = await this.client.json.get(oldDocId);\n if (\n existingDoc &&\n typeof existingDoc === \"object\" &&\n \"created_at\" in existingDoc\n ) {\n createdAt = (existingDoc as any).created_at;\n }\n await this.client.del(oldDocId);\n\n // Also delete associated vector if it exists\n if (this.indexConfig) {\n const oldUuid = oldDocId.split(\":\").pop();\n const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldUuid}`;\n try {\n await this.client.del(oldVectorKey);\n } catch (error) {\n // Vector might not exist\n }\n }\n }\n } catch (error) {\n // Index might not exist yet\n }\n\n // Handle delete operation\n if (value === null) {\n return;\n }\n\n // Store the document\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const doc = {\n prefix,\n key,\n value,\n created_at: createdAt,\n updated_at: now,\n };\n\n await this.client.json.set(storeKey, \"$\", doc);\n\n // Handle embeddings if configured\n if (this.indexConfig && this.embeddings && options?.index !== false) {\n const fieldsToIndex =\n options && Array.isArray(options.index)\n ? options.index\n : this.indexConfig.fields || [\"text\"];\n const textsToEmbed = [];\n const fieldNames = [];\n\n for (const field of fieldsToIndex) {\n if (value[field]) {\n textsToEmbed.push(value[field]);\n fieldNames.push(field);\n }\n }\n\n if (textsToEmbed.length > 0) {\n const embeddings = await this.embeddings.embedDocuments(textsToEmbed);\n\n for (let i = 0; i < embeddings.length; i++) {\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const vectorDoc: VectorDocument = {\n prefix,\n key,\n field_name: fieldNames[i],\n embedding: embeddings[i],\n created_at: now,\n updated_at: now,\n };\n\n await this.client.json.set(vectorKey, \"$\", vectorDoc as any);\n\n // Apply TTL to vector key if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(vectorKey, ttlSeconds);\n }\n }\n }\n }\n\n // Apply TTL if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(storeKey, ttlSeconds);\n }\n }\n\n async delete(namespace: string[], key: string): Promise<void> {\n await this.put(namespace, key, null);\n }\n\n async search(\n namespacePrefix: string[],\n options?: {\n filter?: Filter;\n query?: string;\n limit?: number;\n offset?: number;\n refreshTTL?: boolean;\n similarityThreshold?: number;\n }\n ): Promise<SearchItem[]> {\n const prefix = namespacePrefix.join(\".\");\n const limit = options?.limit || 10;\n const offset = options?.offset || 0;\n\n // Handle vector search if query is provided\n if (options?.query && this.indexConfig && this.embeddings) {\n const [embedding] = await this.embeddings.embedDocuments([options.query]);\n\n // Build KNN query\n // For prefix search, use wildcard since we want to match any document starting with this prefix\n let queryStr = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : \"*\";\n const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);\n\n try {\n // Use KNN query with proper syntax\n const results = await this.client.ft.search(\n \"store_vectors\",\n `(${queryStr})=>[KNN ${limit} @embedding $BLOB]`,\n {\n PARAMS: {\n BLOB: vectorBytes,\n },\n DIALECT: 2,\n LIMIT: { from: offset, size: limit },\n RETURN: [\"prefix\", \"key\", \"__embedding_score\"],\n }\n );\n\n // Get matching store documents\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const docUuid = doc.id.split(\":\").pop();\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n\n const storeDoc = (await this.client.json.get(\n storeKey\n )) as StoreDocument | null;\n if (storeDoc) {\n // Apply advanced filter if provided\n if (options.filter) {\n if (\n !FilterBuilder.matchesFilter(\n storeDoc.value || {},\n options.filter\n )\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options.refreshTTL) {\n await this.refreshItemTTL(storeKey);\n await this.refreshItemTTL(doc.id);\n }\n\n const score = (doc.value as any)?.__embedding_score\n ? this.calculateSimilarityScore(\n parseFloat((doc.value as any).__embedding_score as string)\n )\n : 0;\n\n // Apply similarity threshold if specified\n const threshold =\n options.similarityThreshold ??\n this.indexConfig?.similarityThreshold;\n if (threshold !== undefined && score < threshold) {\n continue;\n }\n\n items.push({\n value: storeDoc.value,\n key: storeDoc.key,\n namespace: storeDoc.prefix.split(\".\"),\n created_at: new Date(storeDoc.created_at / 1000000),\n updated_at: new Date(storeDoc.updated_at / 1000000),\n score,\n });\n }\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n // Regular search without vectors\n let queryStr = \"*\";\n if (prefix) {\n // For prefix search, we need to match all tokens from the namespace prefix\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n // Match all tokens to ensure we get the right prefix\n queryStr = `@prefix:(${tokens.join(\" \")})`;\n }\n }\n\n try {\n const results = await this.client.ft.search(\"store\", queryStr, {\n LIMIT: { from: offset, size: limit },\n SORTBY: { BY: \"created_at\", DIRECTION: \"DESC\" },\n });\n\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n\n // Apply advanced filter\n if (options?.filter) {\n if (\n !FilterBuilder.matchesFilter(jsonDoc.value || {}, options.filter)\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(doc.id);\n }\n\n items.push({\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n });\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async listNamespaces(options?: {\n prefix?: string[];\n suffix?: string[];\n maxDepth?: number;\n limit?: number;\n offset?: number;\n }): Promise<string[][]> {\n let query = \"*\";\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: 1000 }, // Get many to deduplicate\n RETURN: [\"prefix\"],\n });\n\n // Extract unique namespaces and filter\n const namespaceSet = new Set<string>();\n for (const doc of results.documents) {\n const prefix = (doc.value as unknown as StoreDocument).prefix;\n const parts = prefix.split(\".\");\n\n // Apply prefix filter if specified\n if (options?.prefix) {\n // Check if this namespace starts with the specified prefix\n if (parts.length < options.prefix.length) continue;\n\n let matches = true;\n for (let i = 0; i < options.prefix.length; i++) {\n if (parts[i] !== options.prefix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply suffix filter if specified\n if (options?.suffix) {\n // Check if this namespace ends with the specified suffix\n if (parts.length < options.suffix.length) continue;\n\n let matches = true;\n const startIdx = parts.length - options.suffix.length;\n for (let i = 0; i < options.suffix.length; i++) {\n if (parts[startIdx + i] !== options.suffix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply max depth\n if (options?.maxDepth) {\n const truncated = parts.slice(0, options.maxDepth);\n namespaceSet.add(truncated.join(\".\"));\n } else {\n namespaceSet.add(prefix);\n }\n }\n\n // Convert to array of arrays and sort\n let namespaces = Array.from(namespaceSet)\n .map((ns) => ns.split(\".\"))\n .sort((a, b) => a.join(\".\").localeCompare(b.join(\".\")));\n\n // Apply pagination\n if (options?.offset || options?.limit) {\n const offset = options.offset || 0;\n const limit = options.limit || 10;\n namespaces = namespaces.slice(offset, offset + limit);\n }\n\n return namespaces;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async batch(ops: Operation[]): Promise<any[]> {\n const results: any[] = new Array(ops.length).fill(null);\n\n // Process operations in order to maintain dependencies\n for (let idx = 0; idx < ops.length; idx++) {\n const op = ops[idx];\n\n // Execute operation based on type guards\n if (isPutOperation(op)) {\n // TypeScript now knows op is PutOperation\n await this.put(op.namespace, op.key, op.value);\n results[idx] = null;\n } else if (isSearchOperation(op)) {\n // TypeScript now knows op is SearchOperation\n results[idx] = await this.search(op.namespacePrefix, {\n filter: op.filter,\n query: op.query,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isListNamespacesOperation(op)) {\n // TypeScript now knows op is ListNamespacesOperation\n let prefix: string[] | undefined = undefined;\n let suffix: string[] | undefined = undefined;\n\n if (op.matchConditions) {\n for (const condition of op.matchConditions) {\n if (condition.matchType === \"prefix\") {\n prefix = condition.path;\n } else if (condition.matchType === \"suffix\") {\n suffix = condition.path;\n }\n }\n }\n\n results[idx] = await this.listNamespaces({\n prefix,\n suffix,\n maxDepth: op.maxDepth,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isGetOperation(op)) {\n // TypeScript now knows op is GetOperation\n results[idx] = await this.get(op.namespace, op.key);\n } else {\n // This should never happen with proper Operation type\n throw new Error(`Unknown operation type: ${JSON.stringify(op)}`);\n }\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Get statistics about the store.\n * Returns document counts and other metrics.\n */\n async getStatistics(): Promise<{\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n }> {\n const stats: {\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n } = {\n totalDocuments: 0,\n namespaceCount: 0,\n };\n\n try {\n // Get total document count\n const countResult = await this.client.ft.search(\"store\", \"*\", {\n LIMIT: { from: 0, size: 0 },\n });\n stats.totalDocuments = countResult.total || 0;\n\n // Get unique namespace count\n const namespaces = await this.listNamespaces({ limit: 1000 });\n stats.namespaceCount = namespaces.length;\n\n // Get vector document count if index is configured\n if (this.indexConfig) {\n try {\n const vectorResult = await this.client.ft.search(\n \"store_vectors\",\n \"*\",\n {\n LIMIT: { from: 0, size: 0 },\n }\n );\n stats.vectorDocuments = vectorResult.total || 0;\n } catch (error) {\n // Vector index might not exist\n stats.vectorDocuments = 0;\n }\n\n // Get index info\n try {\n stats.indexInfo = await this.client.ft.info(\"store\");\n } catch (error) {\n // Index info might not be available\n }\n }\n } catch (error: any) {\n if (!error.message?.includes(\"no such index\")) {\n throw error;\n }\n }\n\n return stats;\n }\n\n private validateNamespace(namespace: string[]): void {\n if (namespace.length === 0) {\n throw new InvalidNamespaceError(\"Namespace cannot be empty.\");\n }\n for (const label of namespace) {\n // Runtime check for JavaScript users (TypeScript already ensures this)\n // This check is for runtime safety when called from JavaScript\n // noinspection SuspiciousTypeOfGuard\n if (typeof label !== \"string\") {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${String(\n label\n )}' found in ${namespace}. Namespace labels must be strings.`\n );\n }\n if (label.includes(\".\")) {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${label}' found in ${namespace}. Namespace labels cannot contain periods ('.').`\n );\n }\n if (label === \"\") {\n throw new InvalidNamespaceError(\n `Namespace labels cannot be empty strings. Got ${label} in ${namespace}`\n );\n }\n }\n if (namespace[0] === \"langgraph\") {\n throw new InvalidNamespaceError(\n `Root label for namespace cannot be \"langgraph\". Got: ${namespace}`\n );\n }\n }\n\n private async refreshItemTTL(docId: string): Promise<void> {\n if (this.ttlConfig?.defaultTTL) {\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n await this.client.expire(docId, ttlSeconds);\n\n // Also refresh vector key if it exists\n const docUuid = docId.split(\":\").pop();\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n try {\n await this.client.expire(vectorKey, ttlSeconds);\n } catch (error) {\n // Vector key might not exist\n }\n }\n }\n\n private escapeTagValue(value: string): string {\n // Delegate to shared utility for RediSearch TAG field escaping\n return escapeRediSearchTagValue(value);\n }\n\n /**\n * Calculate similarity score based on the distance metric.\n * Converts raw distance to a normalized similarity score [0,1].\n */\n private calculateSimilarityScore(distance: number): number {\n const metric = this.indexConfig?.distanceType || \"cosine\";\n\n switch (metric) {\n case \"cosine\":\n // Cosine distance is in range [0,2], convert to similarity [0,1]\n return Math.max(0, 1 - distance / 2);\n\n case \"l2\":\n // L2 (Euclidean) distance, use exponential decay\n // Similarity = e^(-distance)\n return Math.exp(-distance);\n\n case \"ip\":\n // Inner product can be negative, use sigmoid function\n // Similarity = 1 / (1 + e^(-distance))\n return 1 / (1 + Math.exp(-distance));\n\n default:\n // Default to cosine similarity\n return Math.max(0, 1 - distance / 2);\n }\n }\n}\n\n// Export FilterBuilder for testing purposes\nexport { FilterBuilder };\n"],"mappings":"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"}
package/dist/utils.cjs ADDED
@@ -0,0 +1,22 @@
1
+
2
+ //#region src/utils.ts
3
+ /**
4
+ * Escape special characters in a string for use in RediSearch TAG field queries.
5
+ *
6
+ * RediSearch TAG fields have special characters that need escaping when used
7
+ * within curly braces: , . < > { } [ ] " ' : ; ! @ # $ % ^ & * ( ) - + = ~ | \ ? /
8
+ *
9
+ * This function is used to prevent RediSearch query injection attacks when
10
+ * building queries with user-provided filter values.
11
+ *
12
+ * @param value - The string value to escape
13
+ * @returns The escaped string safe for use in RediSearch TAG queries
14
+ */
15
+ function escapeRediSearchTagValue(value) {
16
+ if (value === "") return "__EMPTY_STRING__";
17
+ return value.replace(/\\/g, "\\\\").replace(/[-\s,.:<>{}[\]"';!@#$%^&*()+=~|?/]/g, "\\$&");
18
+ }
19
+
20
+ //#endregion
21
+ exports.escapeRediSearchTagValue = escapeRediSearchTagValue;
22
+ //# sourceMappingURL=utils.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.cjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["/**\n * Escape special characters in a string for use in RediSearch TAG field queries.\n *\n * RediSearch TAG fields have special characters that need escaping when used\n * within curly braces: , . < > { } [ ] \" ' : ; ! @ # $ % ^ & * ( ) - + = ~ | \\ ? /\n *\n * This function is used to prevent RediSearch query injection attacks when\n * building queries with user-provided filter values.\n *\n * @param value - The string value to escape\n * @returns The escaped string safe for use in RediSearch TAG queries\n */\nexport function escapeRediSearchTagValue(value: string): string {\n // Handle empty string as a special case - use a placeholder\n if (value === \"\") {\n return \"__EMPTY_STRING__\";\n }\n // Escape backslashes first, then all other special characters\n return value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/[-\\s,.:<>{}[\\]\"';!@#$%^&*()+=~|?/]/g, \"\\\\$&\");\n}\n"],"mappings":";;;;;;;;;;;;;;AAYA,SAAgB,yBAAyB,OAAuB;AAE9D,KAAI,UAAU,GACZ,QAAO;AAGT,QAAO,MACJ,QAAQ,OAAO,OAAO,CACtB,QAAQ,uCAAuC,OAAO"}
package/dist/utils.js ADDED
@@ -0,0 +1,21 @@
1
+ //#region src/utils.ts
2
+ /**
3
+ * Escape special characters in a string for use in RediSearch TAG field queries.
4
+ *
5
+ * RediSearch TAG fields have special characters that need escaping when used
6
+ * within curly braces: , . < > { } [ ] " ' : ; ! @ # $ % ^ & * ( ) - + = ~ | \ ? /
7
+ *
8
+ * This function is used to prevent RediSearch query injection attacks when
9
+ * building queries with user-provided filter values.
10
+ *
11
+ * @param value - The string value to escape
12
+ * @returns The escaped string safe for use in RediSearch TAG queries
13
+ */
14
+ function escapeRediSearchTagValue(value) {
15
+ if (value === "") return "__EMPTY_STRING__";
16
+ return value.replace(/\\/g, "\\\\").replace(/[-\s,.:<>{}[\]"';!@#$%^&*()+=~|?/]/g, "\\$&");
17
+ }
18
+
19
+ //#endregion
20
+ export { escapeRediSearchTagValue };
21
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["/**\n * Escape special characters in a string for use in RediSearch TAG field queries.\n *\n * RediSearch TAG fields have special characters that need escaping when used\n * within curly braces: , . < > { } [ ] \" ' : ; ! @ # $ % ^ & * ( ) - + = ~ | \\ ? /\n *\n * This function is used to prevent RediSearch query injection attacks when\n * building queries with user-provided filter values.\n *\n * @param value - The string value to escape\n * @returns The escaped string safe for use in RediSearch TAG queries\n */\nexport function escapeRediSearchTagValue(value: string): string {\n // Handle empty string as a special case - use a placeholder\n if (value === \"\") {\n return \"__EMPTY_STRING__\";\n }\n // Escape backslashes first, then all other special characters\n return value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/[-\\s,.:<>{}[\\]\"';!@#$%^&*()+=~|?/]/g, \"\\\\$&\");\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,yBAAyB,OAAuB;AAE9D,KAAI,UAAU,GACZ,QAAO;AAGT,QAAO,MACJ,QAAQ,OAAO,OAAO,CACtB,QAAQ,uCAAuC,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-checkpoint-redis",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Redis checkpoint and store implementation for LangGraph",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,28 +10,13 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "git@github.com:langchain-ai/langgraphjs.git"
14
- },
15
- "scripts": {
16
- "build": "yarn turbo:command build:internal --filter=@langchain/langgraph-checkpoint-redis",
17
- "build:internal": "yarn workspace @langchain/build compile @langchain/langgraph-checkpoint-redis",
18
- "clean": "rm -rf dist/ dist-cjs/ .turbo/",
19
- "lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/",
20
- "lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/*.ts src/**/*.ts",
21
- "lint": "yarn lint:eslint && yarn lint:dpdm",
22
- "lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm",
23
- "prepublish": "yarn build",
24
- "test": "vitest run",
25
- "test:watch": "vitest watch",
26
- "test:int": "vitest run --mode int",
27
- "format": "prettier --config .prettierrc --write \"src\"",
28
- "format:check": "prettier --config .prettierrc --check \"src\""
13
+ "url": "git+ssh://git@github.com/langchain-ai/langgraphjs.git",
14
+ "directory": "libs/checkpoint-redis"
29
15
  },
30
16
  "author": "Brian Sam-Bodden <bsb@redis.com>",
31
17
  "license": "MIT",
32
18
  "dependencies": {
33
19
  "redis": "^4.7.0",
34
- "testcontainers": "^10.0.0",
35
20
  "ulid": "^2.3.0"
36
21
  },
37
22
  "peerDependencies": {
@@ -39,10 +24,10 @@
39
24
  "@langchain/langgraph-checkpoint": "^1.0.0"
40
25
  },
41
26
  "devDependencies": {
42
- "@langchain/langgraph-checkpoint": "1.0.0",
43
27
  "@langchain/scripts": ">=0.1.2 <0.2.0",
44
28
  "@tsconfig/recommended": "^1.0.3",
45
29
  "@types/node": "^20",
30
+ "testcontainers": "^10.0.0",
46
31
  "@typescript-eslint/eslint-plugin": "^6.12.0",
47
32
  "@typescript-eslint/parser": "^6.12.0",
48
33
  "dotenv": "^16.3.1",
@@ -57,7 +42,8 @@
57
42
  "rollup": "^4.37.0",
58
43
  "tsx": "^4.19.3",
59
44
  "typescript": "^4.9.5 || ^5.4.5",
60
- "vitest": "^3.1.2"
45
+ "vitest": "^3.2.4",
46
+ "@langchain/langgraph-checkpoint": "1.0.0"
61
47
  },
62
48
  "publishConfig": {
63
49
  "access": "public",
@@ -104,5 +90,20 @@
104
90
  },
105
91
  "files": [
106
92
  "dist/"
107
- ]
93
+ ],
94
+ "scripts": {
95
+ "build": "pnpm turbo build:internal --filter=@langchain/langgraph-checkpoint-redis",
96
+ "build:internal": "pnpm --filter @langchain/build compile @langchain/langgraph-checkpoint-redis",
97
+ "clean": "rm -rf dist/ dist-cjs/ .turbo/",
98
+ "lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/",
99
+ "lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/*.ts src/**/*.ts",
100
+ "lint": "pnpm lint:eslint && pnpm lint:dpdm",
101
+ "lint:fix": "pnpm lint:eslint --fix && pnpm lint:dpdm",
102
+ "prepublish": "pnpm build",
103
+ "test": "vitest run",
104
+ "test:watch": "vitest watch",
105
+ "test:int": "vitest run --mode int",
106
+ "format": "prettier --config .prettierrc --write \"src\"",
107
+ "format:check": "prettier --config .prettierrc --check \"src\""
108
+ }
108
109
  }
package/CHANGELOG.md DELETED
@@ -1,18 +0,0 @@
1
- # @langchain/langgraph-checkpoint-redis
2
-
3
- ## 1.0.0
4
-
5
- ### Major Changes
6
-
7
- - 1e1ecbb: This release updates the package for compatibility with LangGraph v1.0. See the [v1.0 release notes](https://docs.langchain.com/oss/javascript/releases/langgraph-v1) for details on what's new.
8
-
9
- ### Patch Changes
10
-
11
- - Updated dependencies [1e1ecbb]
12
- - @langchain/langgraph-checkpoint@1.0.0
13
-
14
- ## 0.0.2
15
-
16
- ### Patch Changes
17
-
18
- - 926db1e: Allow using @langchain/core@^1.0.0-alpha
@@ -1,25 +0,0 @@
1
- //#region rolldown:runtime
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
- key = keys[i];
11
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
- get: ((k) => from[k]).bind(null, key),
13
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
- });
15
- }
16
- return to;
17
- };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
- value: mod,
20
- enumerable: true
21
- }) : target, mod));
22
-
23
- //#endregion
24
-
25
- exports.__toESM = __toESM;