@langchain/langgraph-checkpoint-redis 0.0.1

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.cjs ADDED
@@ -0,0 +1,824 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FilterBuilder = exports.RedisStore = void 0;
4
+ exports.isPutOperation = isPutOperation;
5
+ exports.isGetOperation = isGetOperation;
6
+ exports.isSearchOperation = isSearchOperation;
7
+ exports.isListNamespacesOperation = isListNamespacesOperation;
8
+ const redis_1 = require("redis");
9
+ const uuid_1 = require("uuid");
10
+ const langgraph_checkpoint_1 = require("@langchain/langgraph-checkpoint");
11
+ // Type guard functions for operations
12
+ function isPutOperation(op) {
13
+ return "value" in op && "namespace" in op && "key" in op;
14
+ }
15
+ function isGetOperation(op) {
16
+ return ("namespace" in op &&
17
+ "key" in op &&
18
+ !("value" in op) &&
19
+ !("namespacePrefix" in op) &&
20
+ !("matchConditions" in op));
21
+ }
22
+ function isSearchOperation(op) {
23
+ return "namespacePrefix" in op;
24
+ }
25
+ function isListNamespacesOperation(op) {
26
+ return "matchConditions" in op;
27
+ }
28
+ /**
29
+ * Internal class for evaluating filters against documents.
30
+ * Supports MongoDB-style query operators.
31
+ */
32
+ class FilterBuilder {
33
+ /**
34
+ * Evaluates if a document matches the given filter criteria.
35
+ * Supports advanced operators like $gt, $lt, $in, etc.
36
+ */
37
+ static matchesFilter(doc, filter) {
38
+ for (const [key, filterValue] of Object.entries(filter)) {
39
+ if (!this.matchesFieldFilter(doc, key, filterValue)) {
40
+ return false;
41
+ }
42
+ }
43
+ return true;
44
+ }
45
+ /**
46
+ * Builds a Redis Search query string from filter criteria.
47
+ * Note: This is limited by RediSearch capabilities and may not support all operators.
48
+ */
49
+ static buildRedisSearchQuery(filter, prefix) {
50
+ let queryParts = [];
51
+ let useClientFilter = false;
52
+ // Add prefix filter if provided
53
+ if (prefix) {
54
+ const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
55
+ if (tokens.length > 0) {
56
+ queryParts.push(`@prefix:(${tokens.join(" ")})`);
57
+ }
58
+ }
59
+ // Check if we have complex operators that require client-side filtering
60
+ for (const [_key, value] of Object.entries(filter)) {
61
+ if (typeof value === "object" &&
62
+ value !== null &&
63
+ !Array.isArray(value) &&
64
+ Object.keys(value).some((k) => k.startsWith("$"))) {
65
+ // Complex operators require client-side filtering
66
+ useClientFilter = true;
67
+ break;
68
+ }
69
+ }
70
+ // If no prefix, at least search all documents
71
+ if (queryParts.length === 0) {
72
+ queryParts.push("*");
73
+ }
74
+ return {
75
+ query: queryParts.join(" "),
76
+ useClientFilter,
77
+ };
78
+ }
79
+ static matchesFieldFilter(doc, key, filterValue) {
80
+ // Handle nested keys (e.g., "user.name")
81
+ const actualValue = this.getNestedValue(doc, key);
82
+ // Check if it's an operator object
83
+ if (typeof filterValue === "object" &&
84
+ filterValue !== null &&
85
+ !Array.isArray(filterValue) &&
86
+ Object.keys(filterValue).some((k) => k.startsWith("$"))) {
87
+ // Handle operator object
88
+ return this.matchesOperators(actualValue, filterValue);
89
+ }
90
+ else {
91
+ // Simple equality check
92
+ return this.isEqual(actualValue, filterValue);
93
+ }
94
+ }
95
+ static matchesOperators(actualValue, operators) {
96
+ for (const [operator, operatorValue] of Object.entries(operators)) {
97
+ if (!this.matchesOperator(actualValue, operator, operatorValue)) {
98
+ return false;
99
+ }
100
+ }
101
+ return true;
102
+ }
103
+ static matchesOperator(actualValue, operator, operatorValue) {
104
+ switch (operator) {
105
+ case "$eq":
106
+ return this.isEqual(actualValue, operatorValue);
107
+ case "$ne":
108
+ return !this.isEqual(actualValue, operatorValue);
109
+ case "$gt":
110
+ return (actualValue !== undefined &&
111
+ actualValue !== null &&
112
+ Number(actualValue) > Number(operatorValue));
113
+ case "$gte":
114
+ return (actualValue !== undefined &&
115
+ actualValue !== null &&
116
+ Number(actualValue) >= Number(operatorValue));
117
+ case "$lt":
118
+ return (actualValue !== undefined &&
119
+ actualValue !== null &&
120
+ Number(actualValue) < Number(operatorValue));
121
+ case "$lte":
122
+ return (actualValue !== undefined &&
123
+ actualValue !== null &&
124
+ Number(actualValue) <= Number(operatorValue));
125
+ case "$in":
126
+ if (!Array.isArray(operatorValue))
127
+ return false;
128
+ return operatorValue.some((val) => this.isEqual(actualValue, val));
129
+ case "$nin":
130
+ if (!Array.isArray(operatorValue))
131
+ return false;
132
+ return !operatorValue.some((val) => this.isEqual(actualValue, val));
133
+ case "$exists": {
134
+ const exists = actualValue !== undefined;
135
+ return operatorValue ? exists : !exists;
136
+ }
137
+ default:
138
+ // Unknown operator, return false for safety
139
+ return false;
140
+ }
141
+ }
142
+ static isEqual(a, b) {
143
+ // Handle null and undefined
144
+ if (a === b)
145
+ return true;
146
+ if (a === null || b === null)
147
+ return false;
148
+ if (a === undefined || b === undefined)
149
+ return false;
150
+ // Handle arrays
151
+ if (Array.isArray(a) && Array.isArray(b)) {
152
+ if (a.length !== b.length)
153
+ return false;
154
+ return a.every((val, idx) => this.isEqual(val, b[idx]));
155
+ }
156
+ if (Array.isArray(a) || Array.isArray(b)) {
157
+ // Check if non-array value exists in array
158
+ const arr = Array.isArray(a) ? a : b;
159
+ const val = Array.isArray(a) ? b : a;
160
+ return arr.includes(val);
161
+ }
162
+ // Handle objects
163
+ if (typeof a === "object" && typeof b === "object") {
164
+ const aKeys = Object.keys(a);
165
+ const bKeys = Object.keys(b);
166
+ if (aKeys.length !== bKeys.length)
167
+ return false;
168
+ return aKeys.every((key) => this.isEqual(a[key], b[key]));
169
+ }
170
+ // Primitive comparison (with type coercion for numbers)
171
+ return a == b;
172
+ }
173
+ static getNestedValue(obj, path) {
174
+ const keys = path.split(".");
175
+ let current = obj;
176
+ for (const key of keys) {
177
+ if (current === null || current === undefined) {
178
+ return undefined;
179
+ }
180
+ current = current[key];
181
+ }
182
+ return current;
183
+ }
184
+ }
185
+ exports.FilterBuilder = FilterBuilder;
186
+ const REDIS_KEY_SEPARATOR = ":";
187
+ const STORE_PREFIX = "store";
188
+ const STORE_VECTOR_PREFIX = "store_vectors";
189
+ const SCHEMAS = [
190
+ {
191
+ index: "store",
192
+ prefix: STORE_PREFIX + REDIS_KEY_SEPARATOR,
193
+ schema: {
194
+ "$.prefix": { type: "TEXT", AS: "prefix" },
195
+ "$.key": { type: "TAG", AS: "key" },
196
+ "$.created_at": { type: "NUMERIC", AS: "created_at" },
197
+ "$.updated_at": { type: "NUMERIC", AS: "updated_at" },
198
+ },
199
+ },
200
+ {
201
+ index: "store_vectors",
202
+ prefix: STORE_VECTOR_PREFIX + REDIS_KEY_SEPARATOR,
203
+ schema: {
204
+ "$.prefix": { type: "TEXT", AS: "prefix" },
205
+ "$.key": { type: "TAG", AS: "key" },
206
+ "$.field_name": { type: "TAG", AS: "field_name" },
207
+ "$.embedding": { type: "VECTOR", AS: "embedding" },
208
+ "$.created_at": { type: "NUMERIC", AS: "created_at" },
209
+ "$.updated_at": { type: "NUMERIC", AS: "updated_at" },
210
+ },
211
+ },
212
+ ];
213
+ class RedisStore {
214
+ client;
215
+ indexConfig;
216
+ ttlConfig;
217
+ embeddings;
218
+ constructor(client, config) {
219
+ this.client = client;
220
+ this.indexConfig = config?.index;
221
+ this.ttlConfig = config?.ttl;
222
+ if (this.indexConfig?.embed) {
223
+ this.embeddings = this.indexConfig.embed;
224
+ }
225
+ }
226
+ static async fromConnString(connString, config) {
227
+ const client = (0, redis_1.createClient)({ url: connString });
228
+ await client.connect();
229
+ const store = new RedisStore(client, config);
230
+ await store.setup();
231
+ return store;
232
+ }
233
+ static async fromCluster(rootNodes, config) {
234
+ const client = (0, redis_1.createCluster)({ rootNodes });
235
+ await client.connect();
236
+ const store = new RedisStore(client, config);
237
+ await store.setup();
238
+ return store;
239
+ }
240
+ async setup() {
241
+ // Create store index
242
+ try {
243
+ await this.client.ft.create(SCHEMAS[0].index, SCHEMAS[0].schema, {
244
+ ON: "JSON",
245
+ PREFIX: SCHEMAS[0].prefix,
246
+ });
247
+ }
248
+ catch (error) {
249
+ if (!error.message?.includes("Index already exists")) {
250
+ console.error("Failed to create store index:", error.message);
251
+ }
252
+ }
253
+ // Create vector index if configured
254
+ if (this.indexConfig) {
255
+ const dims = this.indexConfig.dims;
256
+ const distanceMetric = this.indexConfig.distanceType === "cosine"
257
+ ? "COSINE"
258
+ : this.indexConfig.distanceType === "l2"
259
+ ? "L2"
260
+ : this.indexConfig.distanceType === "ip"
261
+ ? "IP"
262
+ : "COSINE";
263
+ // Build schema with correct vector syntax
264
+ const vectorSchema = {
265
+ "$.prefix": { type: "TEXT", AS: "prefix" },
266
+ "$.key": { type: "TAG", AS: "key" },
267
+ "$.field_name": { type: "TAG", AS: "field_name" },
268
+ "$.created_at": { type: "NUMERIC", AS: "created_at" },
269
+ "$.updated_at": { type: "NUMERIC", AS: "updated_at" },
270
+ };
271
+ // Add vector field with correct syntax
272
+ vectorSchema["$.embedding"] = {
273
+ type: "VECTOR",
274
+ ALGORITHM: "FLAT",
275
+ TYPE: "FLOAT32",
276
+ DIM: dims,
277
+ DISTANCE_METRIC: distanceMetric,
278
+ AS: "embedding",
279
+ };
280
+ try {
281
+ await this.client.ft.create(SCHEMAS[1].index, vectorSchema, {
282
+ ON: "JSON",
283
+ PREFIX: SCHEMAS[1].prefix,
284
+ });
285
+ }
286
+ catch (error) {
287
+ if (!error.message?.includes("Index already exists")) {
288
+ console.error("Failed to create vector index:", error.message);
289
+ }
290
+ }
291
+ }
292
+ }
293
+ async get(namespace, key, options) {
294
+ const prefix = namespace.join(".");
295
+ // For TEXT fields, we need to match all tokens (split by dots and hyphens)
296
+ const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
297
+ const prefixQuery = tokens.length > 0 ? `@prefix:(${tokens.join(" ")})` : "*";
298
+ // For TAG fields in curly braces, escape special characters
299
+ // Handle empty string as a special case
300
+ let query;
301
+ if (key === "") {
302
+ // For empty keys, search by prefix and filter results
303
+ query = prefixQuery;
304
+ }
305
+ else {
306
+ const escapedKey = this.escapeTagValue(key);
307
+ query = `(${prefixQuery}) (@key:{${escapedKey}})`;
308
+ }
309
+ try {
310
+ const results = await this.client.ft.search("store", query, {
311
+ LIMIT: { from: 0, size: key === "" ? 100 : 1 },
312
+ });
313
+ if (!results || !results.documents || results.documents.length === 0) {
314
+ return null;
315
+ }
316
+ // For empty key, filter to find the exact match
317
+ if (key === "") {
318
+ for (const doc of results.documents) {
319
+ const jsonDoc = doc.value;
320
+ if (jsonDoc.key === "" && jsonDoc.prefix === prefix) {
321
+ const docId = doc.id;
322
+ // Refresh TTL if requested
323
+ if (options?.refreshTTL) {
324
+ await this.refreshItemTTL(docId);
325
+ }
326
+ return {
327
+ value: jsonDoc.value,
328
+ key: jsonDoc.key,
329
+ namespace: jsonDoc.prefix.split("."),
330
+ created_at: new Date(jsonDoc.created_at / 1000000),
331
+ updated_at: new Date(jsonDoc.updated_at / 1000000),
332
+ };
333
+ }
334
+ }
335
+ return null;
336
+ }
337
+ const doc = results.documents[0];
338
+ const jsonDoc = doc.value;
339
+ const docId = doc.id;
340
+ // Refresh TTL if requested
341
+ if (options?.refreshTTL) {
342
+ await this.refreshItemTTL(docId);
343
+ }
344
+ return {
345
+ value: jsonDoc.value,
346
+ key: jsonDoc.key,
347
+ namespace: jsonDoc.prefix.split("."),
348
+ created_at: new Date(jsonDoc.created_at / 1000000),
349
+ updated_at: new Date(jsonDoc.updated_at / 1000000),
350
+ };
351
+ }
352
+ catch (error) {
353
+ if (error.message?.includes("no such index")) {
354
+ return null;
355
+ }
356
+ throw error;
357
+ }
358
+ }
359
+ async put(namespace, key, value, options) {
360
+ // Validate namespace for put operations
361
+ this.validateNamespace(namespace);
362
+ const prefix = namespace.join(".");
363
+ const docId = (0, uuid_1.v4)();
364
+ // Use high-resolution time for better timestamp precision
365
+ const now = Date.now() * 1000000 + Math.floor(performance.now() * 1000); // Microseconds + nanoseconds component
366
+ let createdAt = now; // Will be overridden if document exists
367
+ // Delete existing document if it exists
368
+ // For TEXT fields, we need to match all tokens (split by dots and hyphens)
369
+ const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
370
+ const prefixQuery = tokens.length > 0 ? `@prefix:(${tokens.join(" ")})` : "*";
371
+ // For TAG fields in curly braces, escape special characters
372
+ const escapedKey = this.escapeTagValue(key);
373
+ const existingQuery = `(${prefixQuery}) (@key:{${escapedKey}})`;
374
+ try {
375
+ const existing = await this.client.ft.search("store", existingQuery, {
376
+ LIMIT: { from: 0, size: 1 },
377
+ });
378
+ if (existing && existing.documents && existing.documents.length > 0) {
379
+ const oldDocId = existing.documents[0].id;
380
+ // Preserve the original created_at timestamp
381
+ const existingDoc = await this.client.json.get(oldDocId);
382
+ if (existingDoc &&
383
+ typeof existingDoc === "object" &&
384
+ "created_at" in existingDoc) {
385
+ createdAt = existingDoc.created_at;
386
+ }
387
+ await this.client.del(oldDocId);
388
+ // Also delete associated vector if it exists
389
+ if (this.indexConfig) {
390
+ const oldUuid = oldDocId.split(":").pop();
391
+ const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldUuid}`;
392
+ try {
393
+ await this.client.del(oldVectorKey);
394
+ }
395
+ catch (error) {
396
+ // Vector might not exist
397
+ }
398
+ }
399
+ }
400
+ }
401
+ catch (error) {
402
+ // Index might not exist yet
403
+ }
404
+ // Handle delete operation
405
+ if (value === null) {
406
+ return;
407
+ }
408
+ // Store the document
409
+ const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;
410
+ const doc = {
411
+ prefix,
412
+ key,
413
+ value,
414
+ created_at: createdAt,
415
+ updated_at: now,
416
+ };
417
+ await this.client.json.set(storeKey, "$", doc);
418
+ // Handle embeddings if configured
419
+ if (this.indexConfig && this.embeddings && options?.index !== false) {
420
+ const fieldsToIndex = options && Array.isArray(options.index)
421
+ ? options.index
422
+ : this.indexConfig.fields || ["text"];
423
+ const textsToEmbed = [];
424
+ const fieldNames = [];
425
+ for (const field of fieldsToIndex) {
426
+ if (value[field]) {
427
+ textsToEmbed.push(value[field]);
428
+ fieldNames.push(field);
429
+ }
430
+ }
431
+ if (textsToEmbed.length > 0) {
432
+ const embeddings = await this.embeddings.embedDocuments(textsToEmbed);
433
+ for (let i = 0; i < embeddings.length; i++) {
434
+ const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;
435
+ const vectorDoc = {
436
+ prefix,
437
+ key,
438
+ field_name: fieldNames[i],
439
+ embedding: embeddings[i],
440
+ created_at: now,
441
+ updated_at: now,
442
+ };
443
+ await this.client.json.set(vectorKey, "$", vectorDoc);
444
+ // Apply TTL to vector key if configured
445
+ const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;
446
+ if (ttlMinutes) {
447
+ const ttlSeconds = Math.floor(ttlMinutes * 60);
448
+ await this.client.expire(vectorKey, ttlSeconds);
449
+ }
450
+ }
451
+ }
452
+ }
453
+ // Apply TTL if configured
454
+ const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;
455
+ if (ttlMinutes) {
456
+ const ttlSeconds = Math.floor(ttlMinutes * 60);
457
+ await this.client.expire(storeKey, ttlSeconds);
458
+ }
459
+ }
460
+ async delete(namespace, key) {
461
+ await this.put(namespace, key, null);
462
+ }
463
+ async search(namespacePrefix, options) {
464
+ const prefix = namespacePrefix.join(".");
465
+ const limit = options?.limit || 10;
466
+ const offset = options?.offset || 0;
467
+ // Handle vector search if query is provided
468
+ if (options?.query && this.indexConfig && this.embeddings) {
469
+ const [embedding] = await this.embeddings.embedDocuments([options.query]);
470
+ // Build KNN query
471
+ // For prefix search, use wildcard since we want to match any document starting with this prefix
472
+ let queryStr = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : "*";
473
+ const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);
474
+ try {
475
+ // Use KNN query with proper syntax
476
+ const results = await this.client.ft.search("store_vectors", `(${queryStr})=>[KNN ${limit} @embedding $BLOB]`, {
477
+ PARAMS: {
478
+ BLOB: vectorBytes,
479
+ },
480
+ DIALECT: 2,
481
+ LIMIT: { from: offset, size: limit },
482
+ RETURN: ["prefix", "key", "__embedding_score"],
483
+ });
484
+ // Get matching store documents
485
+ const items = [];
486
+ for (const doc of results.documents) {
487
+ const docUuid = doc.id.split(":").pop();
488
+ const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;
489
+ const storeDoc = (await this.client.json.get(storeKey));
490
+ if (storeDoc) {
491
+ // Apply advanced filter if provided
492
+ if (options.filter) {
493
+ if (!FilterBuilder.matchesFilter(storeDoc.value || {}, options.filter)) {
494
+ continue;
495
+ }
496
+ }
497
+ // Refresh TTL if requested
498
+ if (options.refreshTTL) {
499
+ await this.refreshItemTTL(storeKey);
500
+ await this.refreshItemTTL(doc.id);
501
+ }
502
+ const score = doc.value?.__embedding_score
503
+ ? this.calculateSimilarityScore(parseFloat(doc.value.__embedding_score))
504
+ : 0;
505
+ // Apply similarity threshold if specified
506
+ const threshold = options.similarityThreshold ??
507
+ this.indexConfig?.similarityThreshold;
508
+ if (threshold !== undefined && score < threshold) {
509
+ continue;
510
+ }
511
+ items.push({
512
+ value: storeDoc.value,
513
+ key: storeDoc.key,
514
+ namespace: storeDoc.prefix.split("."),
515
+ created_at: new Date(storeDoc.created_at / 1000000),
516
+ updated_at: new Date(storeDoc.updated_at / 1000000),
517
+ score,
518
+ });
519
+ }
520
+ }
521
+ return items;
522
+ }
523
+ catch (error) {
524
+ if (error.message?.includes("no such index")) {
525
+ return [];
526
+ }
527
+ throw error;
528
+ }
529
+ }
530
+ // Regular search without vectors
531
+ let queryStr = "*";
532
+ if (prefix) {
533
+ // For prefix search, we need to match all tokens from the namespace prefix
534
+ const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);
535
+ if (tokens.length > 0) {
536
+ // Match all tokens to ensure we get the right prefix
537
+ queryStr = `@prefix:(${tokens.join(" ")})`;
538
+ }
539
+ }
540
+ try {
541
+ const results = await this.client.ft.search("store", queryStr, {
542
+ LIMIT: { from: offset, size: limit },
543
+ SORTBY: { BY: "created_at", DIRECTION: "DESC" },
544
+ });
545
+ const items = [];
546
+ for (const doc of results.documents) {
547
+ const jsonDoc = doc.value;
548
+ // Apply advanced filter
549
+ if (options?.filter) {
550
+ if (!FilterBuilder.matchesFilter(jsonDoc.value || {}, options.filter)) {
551
+ continue;
552
+ }
553
+ }
554
+ // Refresh TTL if requested
555
+ if (options?.refreshTTL) {
556
+ await this.refreshItemTTL(doc.id);
557
+ }
558
+ items.push({
559
+ value: jsonDoc.value,
560
+ key: jsonDoc.key,
561
+ namespace: jsonDoc.prefix.split("."),
562
+ created_at: new Date(jsonDoc.created_at / 1000000),
563
+ updated_at: new Date(jsonDoc.updated_at / 1000000),
564
+ });
565
+ }
566
+ return items;
567
+ }
568
+ catch (error) {
569
+ if (error.message?.includes("no such index")) {
570
+ return [];
571
+ }
572
+ throw error;
573
+ }
574
+ }
575
+ async listNamespaces(options) {
576
+ let query = "*";
577
+ try {
578
+ const results = await this.client.ft.search("store", query, {
579
+ LIMIT: { from: 0, size: 1000 }, // Get many to deduplicate
580
+ RETURN: ["prefix"],
581
+ });
582
+ // Extract unique namespaces and filter
583
+ const namespaceSet = new Set();
584
+ for (const doc of results.documents) {
585
+ const prefix = doc.value.prefix;
586
+ const parts = prefix.split(".");
587
+ // Apply prefix filter if specified
588
+ if (options?.prefix) {
589
+ // Check if this namespace starts with the specified prefix
590
+ if (parts.length < options.prefix.length)
591
+ continue;
592
+ let matches = true;
593
+ for (let i = 0; i < options.prefix.length; i++) {
594
+ if (parts[i] !== options.prefix[i]) {
595
+ matches = false;
596
+ break;
597
+ }
598
+ }
599
+ if (!matches)
600
+ continue;
601
+ }
602
+ // Apply suffix filter if specified
603
+ if (options?.suffix) {
604
+ // Check if this namespace ends with the specified suffix
605
+ if (parts.length < options.suffix.length)
606
+ continue;
607
+ let matches = true;
608
+ const startIdx = parts.length - options.suffix.length;
609
+ for (let i = 0; i < options.suffix.length; i++) {
610
+ if (parts[startIdx + i] !== options.suffix[i]) {
611
+ matches = false;
612
+ break;
613
+ }
614
+ }
615
+ if (!matches)
616
+ continue;
617
+ }
618
+ // Apply max depth
619
+ if (options?.maxDepth) {
620
+ const truncated = parts.slice(0, options.maxDepth);
621
+ namespaceSet.add(truncated.join("."));
622
+ }
623
+ else {
624
+ namespaceSet.add(prefix);
625
+ }
626
+ }
627
+ // Convert to array of arrays and sort
628
+ let namespaces = Array.from(namespaceSet)
629
+ .map((ns) => ns.split("."))
630
+ .sort((a, b) => a.join(".").localeCompare(b.join(".")));
631
+ // Apply pagination
632
+ if (options?.offset || options?.limit) {
633
+ const offset = options.offset || 0;
634
+ const limit = options.limit || 10;
635
+ namespaces = namespaces.slice(offset, offset + limit);
636
+ }
637
+ return namespaces;
638
+ }
639
+ catch (error) {
640
+ if (error.message?.includes("no such index")) {
641
+ return [];
642
+ }
643
+ throw error;
644
+ }
645
+ }
646
+ async batch(ops) {
647
+ const results = new Array(ops.length).fill(null);
648
+ // Process operations in order to maintain dependencies
649
+ for (let idx = 0; idx < ops.length; idx++) {
650
+ const op = ops[idx];
651
+ // Execute operation based on type guards
652
+ if (isPutOperation(op)) {
653
+ // TypeScript now knows op is PutOperation
654
+ await this.put(op.namespace, op.key, op.value);
655
+ results[idx] = null;
656
+ }
657
+ else if (isSearchOperation(op)) {
658
+ // TypeScript now knows op is SearchOperation
659
+ results[idx] = await this.search(op.namespacePrefix, {
660
+ filter: op.filter,
661
+ query: op.query,
662
+ limit: op.limit,
663
+ offset: op.offset,
664
+ });
665
+ }
666
+ else if (isListNamespacesOperation(op)) {
667
+ // TypeScript now knows op is ListNamespacesOperation
668
+ let prefix = undefined;
669
+ let suffix = undefined;
670
+ if (op.matchConditions) {
671
+ for (const condition of op.matchConditions) {
672
+ if (condition.matchType === "prefix") {
673
+ prefix = condition.path;
674
+ }
675
+ else if (condition.matchType === "suffix") {
676
+ suffix = condition.path;
677
+ }
678
+ }
679
+ }
680
+ results[idx] = await this.listNamespaces({
681
+ prefix,
682
+ suffix,
683
+ maxDepth: op.maxDepth,
684
+ limit: op.limit,
685
+ offset: op.offset,
686
+ });
687
+ }
688
+ else if (isGetOperation(op)) {
689
+ // TypeScript now knows op is GetOperation
690
+ results[idx] = await this.get(op.namespace, op.key);
691
+ }
692
+ else {
693
+ // This should never happen with proper Operation type
694
+ throw new Error(`Unknown operation type: ${JSON.stringify(op)}`);
695
+ }
696
+ }
697
+ return results;
698
+ }
699
+ async close() {
700
+ await this.client.quit();
701
+ }
702
+ /**
703
+ * Get statistics about the store.
704
+ * Returns document counts and other metrics.
705
+ */
706
+ async getStatistics() {
707
+ const stats = {
708
+ totalDocuments: 0,
709
+ namespaceCount: 0,
710
+ };
711
+ try {
712
+ // Get total document count
713
+ const countResult = await this.client.ft.search("store", "*", {
714
+ LIMIT: { from: 0, size: 0 },
715
+ });
716
+ stats.totalDocuments = countResult.total || 0;
717
+ // Get unique namespace count
718
+ const namespaces = await this.listNamespaces({ limit: 1000 });
719
+ stats.namespaceCount = namespaces.length;
720
+ // Get vector document count if index is configured
721
+ if (this.indexConfig) {
722
+ try {
723
+ const vectorResult = await this.client.ft.search("store_vectors", "*", {
724
+ LIMIT: { from: 0, size: 0 },
725
+ });
726
+ stats.vectorDocuments = vectorResult.total || 0;
727
+ }
728
+ catch (error) {
729
+ // Vector index might not exist
730
+ stats.vectorDocuments = 0;
731
+ }
732
+ // Get index info
733
+ try {
734
+ stats.indexInfo = await this.client.ft.info("store");
735
+ }
736
+ catch (error) {
737
+ // Index info might not be available
738
+ }
739
+ }
740
+ }
741
+ catch (error) {
742
+ if (!error.message?.includes("no such index")) {
743
+ throw error;
744
+ }
745
+ }
746
+ return stats;
747
+ }
748
+ validateNamespace(namespace) {
749
+ if (namespace.length === 0) {
750
+ throw new langgraph_checkpoint_1.InvalidNamespaceError("Namespace cannot be empty.");
751
+ }
752
+ for (const label of namespace) {
753
+ // Runtime check for JavaScript users (TypeScript already ensures this)
754
+ // This check is for runtime safety when called from JavaScript
755
+ // noinspection SuspiciousTypeOfGuard
756
+ if (typeof label !== "string") {
757
+ throw new langgraph_checkpoint_1.InvalidNamespaceError(`Invalid namespace label '${String(label)}' found in ${namespace}. Namespace labels must be strings.`);
758
+ }
759
+ if (label.includes(".")) {
760
+ throw new langgraph_checkpoint_1.InvalidNamespaceError(`Invalid namespace label '${label}' found in ${namespace}. Namespace labels cannot contain periods ('.').`);
761
+ }
762
+ if (label === "") {
763
+ throw new langgraph_checkpoint_1.InvalidNamespaceError(`Namespace labels cannot be empty strings. Got ${label} in ${namespace}`);
764
+ }
765
+ }
766
+ if (namespace[0] === "langgraph") {
767
+ throw new langgraph_checkpoint_1.InvalidNamespaceError(`Root label for namespace cannot be "langgraph". Got: ${namespace}`);
768
+ }
769
+ }
770
+ async refreshItemTTL(docId) {
771
+ if (this.ttlConfig?.defaultTTL) {
772
+ const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
773
+ await this.client.expire(docId, ttlSeconds);
774
+ // Also refresh vector key if it exists
775
+ const docUuid = docId.split(":").pop();
776
+ const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;
777
+ try {
778
+ await this.client.expire(vectorKey, ttlSeconds);
779
+ }
780
+ catch (error) {
781
+ // Vector key might not exist
782
+ }
783
+ }
784
+ }
785
+ escapeTagValue(value) {
786
+ // For TAG fields, we need to escape special characters
787
+ // Based on RediSearch documentation, these characters need escaping in TAG fields
788
+ // when used within curly braces: , . < > { } [ ] " ' : ; ! @ # $ % ^ & * ( ) - + = ~ | \ ? /
789
+ // Handle empty string as a special case - use a placeholder
790
+ if (value === "") {
791
+ // Use a special placeholder for empty strings
792
+ return "__EMPTY_STRING__";
793
+ }
794
+ // We'll escape the most common ones that appear in keys
795
+ return value
796
+ .replace(/\\/g, "\\\\")
797
+ .replace(/[-\s,.:<>{}[\]"';!@#$%^&*()+=~|?/]/g, "\\$&");
798
+ }
799
+ /**
800
+ * Calculate similarity score based on the distance metric.
801
+ * Converts raw distance to a normalized similarity score [0,1].
802
+ */
803
+ calculateSimilarityScore(distance) {
804
+ const metric = this.indexConfig?.distanceType || "cosine";
805
+ switch (metric) {
806
+ case "cosine":
807
+ // Cosine distance is in range [0,2], convert to similarity [0,1]
808
+ return Math.max(0, 1 - distance / 2);
809
+ case "l2":
810
+ // L2 (Euclidean) distance, use exponential decay
811
+ // Similarity = e^(-distance)
812
+ return Math.exp(-distance);
813
+ case "ip":
814
+ // Inner product can be negative, use sigmoid function
815
+ // Similarity = 1 / (1 + e^(-distance))
816
+ return 1 / (1 + Math.exp(-distance));
817
+ default:
818
+ // Default to cosine similarity
819
+ return Math.max(0, 1 - distance / 2);
820
+ }
821
+ }
822
+ }
823
+ exports.RedisStore = RedisStore;
824
+ //# sourceMappingURL=store.js.map