@mastra/redis 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1935 +1,1684 @@
1
- import { MessageList } from '@mastra/core/agent';
2
- import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
3
- import { MemoryStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES, ensureDate, createStorageErrorId, normalizePerPage, calculatePagination, jsonValueEquals, filterByDateRange, ScoresStorage, TABLE_SCORERS, transformScoreRow, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, MastraStorage, serializeDate } from '@mastra/core/storage';
4
- import crypto2 from 'crypto';
5
- import { saveScorePayloadSchema } from '@mastra/core/evals';
6
- import { createClient } from 'redis';
7
- import { MastraServerCache } from '@mastra/core/cache';
8
-
9
- // src/storage/domains/memory/index.ts
1
+ import { MessageList } from "@mastra/core/agent";
2
+ import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
+ import { MastraStorage, MemoryStorage, ScoresStorage, TABLE_MESSAGES, TABLE_RESOURCES, TABLE_SCORERS, TABLE_THREADS, TABLE_WORKFLOW_SNAPSHOT, WorkflowsStorage, calculatePagination, createStorageErrorId, ensureDate, filterByDateRange, jsonValueEquals, normalizePerPage, serializeDate, storageMessageMatchesMetadataFilter, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
+ import crypto$1 from "crypto";
5
+ import { saveScorePayloadSchema } from "@mastra/core/evals";
6
+ import { createClient } from "redis";
7
+ import { MastraServerCache } from "@mastra/core/cache";
8
+ //#region src/storage/domains/utils.ts
9
+ /**
10
+ * Generate a Redis key from table name and key parts.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * getKey('mastra_threads', { id: 'thread-123' });
15
+ * // Returns: 'mastra_threads:id:thread-123'
16
+ *
17
+ * getKey('mastra_messages', { threadId: 'thread-123', id: 'msg-456' });
18
+ * // Returns: 'mastra_messages:threadId:thread-123:id:msg-456'
19
+ * ```
20
+ */
10
21
  function getKey(tableName, keys) {
11
- const keyParts = Object.entries(keys).filter(([_, value]) => value !== void 0).map(([key, value]) => {
12
- if (value && typeof value === "object") {
13
- return `${key}:${JSON.stringify(value)}`;
14
- }
15
- return `${key}:${value}`;
16
- });
17
- return `${tableName}:${keyParts.join(":")}`;
22
+ return `${tableName}:${Object.entries(keys).filter(([_, value]) => value !== void 0).map(([key, value]) => {
23
+ if (value && typeof value === "object") return `${key}:${JSON.stringify(value)}`;
24
+ return `${key}:${value}`;
25
+ }).join(":")}`;
18
26
  }
27
+ /**
28
+ * Process a record for storage, generating the appropriate key and serializing dates.
29
+ */
19
30
  function processRecord(tableName, record) {
20
- let key;
21
- if (tableName === TABLE_MESSAGES) {
22
- key = getKey(tableName, { threadId: record.threadId, id: record.id });
23
- } else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
24
- key = getKey(tableName, {
25
- namespace: record.namespace || "workflows",
26
- workflow_name: record.workflow_name,
27
- run_id: record.run_id,
28
- ...record.resourceId ? { resourceId: record.resourceId } : {}
29
- });
30
- } else {
31
- key = getKey(tableName, { id: record.id });
32
- }
33
- const processedRecord = {
34
- ...record,
35
- createdAt: serializeDate(record.createdAt),
36
- updatedAt: serializeDate(record.updatedAt)
37
- };
38
- return { key, processedRecord };
31
+ let key;
32
+ if (tableName === TABLE_MESSAGES) key = getKey(tableName, {
33
+ threadId: record.threadId,
34
+ id: record.id
35
+ });
36
+ else if (tableName === TABLE_WORKFLOW_SNAPSHOT) key = getKey(tableName, {
37
+ namespace: record.namespace || "workflows",
38
+ workflow_name: record.workflow_name,
39
+ run_id: record.run_id,
40
+ ...record.resourceId ? { resourceId: record.resourceId } : {}
41
+ });
42
+ else key = getKey(tableName, { id: record.id });
43
+ const processedRecord = {
44
+ ...record,
45
+ createdAt: serializeDate(record.createdAt),
46
+ updatedAt: serializeDate(record.updatedAt)
47
+ };
48
+ return {
49
+ key,
50
+ processedRecord
51
+ };
39
52
  }
40
-
41
- // src/storage/db/index.ts
53
+ //#endregion
54
+ //#region src/storage/db/index.ts
42
55
  var RedisDB = class {
43
- client;
44
- constructor({ client }) {
45
- this.client = client;
46
- }
47
- getClient() {
48
- return this.client;
49
- }
50
- async insert({ tableName, record }) {
51
- const { key, processedRecord } = processRecord(tableName, record);
52
- try {
53
- await this.client.set(key, JSON.stringify(processedRecord));
54
- } catch (error) {
55
- throw new MastraError(
56
- {
57
- id: createStorageErrorId("REDIS", "INSERT", "FAILED"),
58
- domain: ErrorDomain.STORAGE,
59
- category: ErrorCategory.THIRD_PARTY,
60
- details: {
61
- tableName
62
- }
63
- },
64
- error
65
- );
66
- }
67
- }
68
- async get({ tableName, keys }) {
69
- const key = getKey(tableName, keys);
70
- try {
71
- const data = await this.client.get(key);
72
- if (!data) {
73
- return null;
74
- }
75
- return JSON.parse(data);
76
- } catch (error) {
77
- throw new MastraError(
78
- {
79
- id: createStorageErrorId("REDIS", "LOAD", "FAILED"),
80
- domain: ErrorDomain.STORAGE,
81
- category: ErrorCategory.THIRD_PARTY,
82
- details: {
83
- tableName
84
- }
85
- },
86
- error
87
- );
88
- }
89
- }
90
- async scanAndDelete(pattern, batchSize = 1e4) {
91
- let cursor = "0";
92
- let totalDeleted = 0;
93
- do {
94
- const result = await this.client.scan(cursor, { MATCH: pattern, COUNT: batchSize });
95
- if (result.keys.length > 0) {
96
- await this.client.del(result.keys);
97
- totalDeleted += result.keys.length;
98
- }
99
- cursor = result.cursor;
100
- } while (cursor !== "0");
101
- return totalDeleted;
102
- }
103
- async scanKeys(pattern, batchSize = 1e4) {
104
- let cursor = "0";
105
- const keys = [];
106
- do {
107
- const result = await this.client.scan(cursor, { MATCH: pattern, COUNT: batchSize });
108
- keys.push(...result.keys);
109
- cursor = result.cursor;
110
- } while (cursor !== "0");
111
- return keys;
112
- }
113
- async deleteData({ tableName }) {
114
- const pattern = `${tableName}:*`;
115
- try {
116
- await this.scanAndDelete(pattern);
117
- } catch (error) {
118
- throw new MastraError(
119
- {
120
- id: createStorageErrorId("REDIS", "CLEAR_TABLE", "FAILED"),
121
- domain: ErrorDomain.STORAGE,
122
- category: ErrorCategory.THIRD_PARTY,
123
- details: {
124
- tableName
125
- }
126
- },
127
- error
128
- );
129
- }
130
- }
56
+ client;
57
+ constructor({ client }) {
58
+ this.client = client;
59
+ }
60
+ getClient() {
61
+ return this.client;
62
+ }
63
+ async insert({ tableName, record }) {
64
+ const { key, processedRecord } = processRecord(tableName, record);
65
+ try {
66
+ await this.client.set(key, JSON.stringify(processedRecord));
67
+ } catch (error) {
68
+ throw new MastraError({
69
+ id: createStorageErrorId("REDIS", "INSERT", "FAILED"),
70
+ domain: ErrorDomain.STORAGE,
71
+ category: ErrorCategory.THIRD_PARTY,
72
+ details: { tableName }
73
+ }, error);
74
+ }
75
+ }
76
+ async get({ tableName, keys }) {
77
+ const key = getKey(tableName, keys);
78
+ try {
79
+ const data = await this.client.get(key);
80
+ if (!data) return null;
81
+ return JSON.parse(data);
82
+ } catch (error) {
83
+ throw new MastraError({
84
+ id: createStorageErrorId("REDIS", "LOAD", "FAILED"),
85
+ domain: ErrorDomain.STORAGE,
86
+ category: ErrorCategory.THIRD_PARTY,
87
+ details: { tableName }
88
+ }, error);
89
+ }
90
+ }
91
+ async scanAndDelete(pattern, batchSize = 1e4) {
92
+ let cursor = "0";
93
+ let totalDeleted = 0;
94
+ do {
95
+ const result = await this.client.scan(cursor, {
96
+ MATCH: pattern,
97
+ COUNT: batchSize
98
+ });
99
+ if (result.keys.length > 0) {
100
+ await this.client.del(result.keys);
101
+ totalDeleted += result.keys.length;
102
+ }
103
+ cursor = result.cursor;
104
+ } while (cursor !== "0");
105
+ return totalDeleted;
106
+ }
107
+ async scanKeys(pattern, batchSize = 1e4) {
108
+ let cursor = "0";
109
+ const keys = [];
110
+ do {
111
+ const result = await this.client.scan(cursor, {
112
+ MATCH: pattern,
113
+ COUNT: batchSize
114
+ });
115
+ keys.push(...result.keys);
116
+ cursor = result.cursor;
117
+ } while (cursor !== "0");
118
+ return keys;
119
+ }
120
+ async deleteData({ tableName }) {
121
+ const pattern = `${tableName}:*`;
122
+ try {
123
+ await this.scanAndDelete(pattern);
124
+ } catch (error) {
125
+ throw new MastraError({
126
+ id: createStorageErrorId("REDIS", "CLEAR_TABLE", "FAILED"),
127
+ domain: ErrorDomain.STORAGE,
128
+ category: ErrorCategory.THIRD_PARTY,
129
+ details: { tableName }
130
+ }, error);
131
+ }
132
+ }
131
133
  };
132
-
133
- // src/storage/domains/memory/index.ts
134
+ //#endregion
135
+ //#region src/storage/domains/memory/index.ts
134
136
  var StoreMemoryRedis = class extends MemoryStorage {
135
- client;
136
- db;
137
- constructor(config) {
138
- super();
139
- this.client = config.client;
140
- this.db = new RedisDB({ client: config.client });
141
- }
142
- async dangerouslyClearAll() {
143
- await this.db.deleteData({ tableName: TABLE_THREADS });
144
- await this.db.deleteData({ tableName: TABLE_MESSAGES });
145
- await this.db.deleteData({ tableName: TABLE_RESOURCES });
146
- await this.db.scanAndDelete("msg-idx:*");
147
- await this.db.scanAndDelete("thread:*:messages");
148
- }
149
- async getThreadById({
150
- threadId,
151
- resourceId
152
- }) {
153
- try {
154
- const thread = await this.db.get({
155
- tableName: TABLE_THREADS,
156
- keys: { id: threadId }
157
- });
158
- if (!thread || resourceId !== void 0 && thread.resourceId !== resourceId) {
159
- return null;
160
- }
161
- return {
162
- ...thread,
163
- createdAt: ensureDate(thread.createdAt),
164
- updatedAt: ensureDate(thread.updatedAt),
165
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata
166
- };
167
- } catch (error) {
168
- throw new MastraError(
169
- {
170
- id: createStorageErrorId("REDIS", "GET_THREAD_BY_ID", "FAILED"),
171
- domain: ErrorDomain.STORAGE,
172
- category: ErrorCategory.THIRD_PARTY,
173
- details: {
174
- threadId
175
- }
176
- },
177
- error
178
- );
179
- }
180
- }
181
- async listThreadsByResourceId(args) {
182
- return this.listThreads(args);
183
- }
184
- async listThreads(args) {
185
- const { page = 0, perPage: perPageInput, orderBy, filter } = args;
186
- const { field, direction } = this.parseOrderBy(orderBy);
187
- try {
188
- this.validatePaginationInput(page, perPageInput ?? 100);
189
- } catch (error) {
190
- throw new MastraError(
191
- {
192
- id: createStorageErrorId("REDIS", "LIST_THREADS", "INVALID_PAGE"),
193
- domain: ErrorDomain.STORAGE,
194
- category: ErrorCategory.USER,
195
- details: { page, ...perPageInput !== void 0 && { perPage: perPageInput } }
196
- },
197
- error instanceof Error ? error : new Error("Invalid pagination parameters")
198
- );
199
- }
200
- const perPage = normalizePerPage(perPageInput, 100);
201
- try {
202
- this.validateMetadataKeys(filter?.metadata);
203
- } catch (error) {
204
- throw new MastraError(
205
- {
206
- id: createStorageErrorId("REDIS", "LIST_THREADS", "INVALID_METADATA_KEY"),
207
- domain: ErrorDomain.STORAGE,
208
- category: ErrorCategory.USER,
209
- details: { metadataKeys: filter?.metadata ? Object.keys(filter.metadata).join(", ") : "" }
210
- },
211
- error instanceof Error ? error : new Error("Invalid metadata key")
212
- );
213
- }
214
- const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
215
- try {
216
- let allThreads = [];
217
- const pattern = `${TABLE_THREADS}:*`;
218
- const keys = await this.db.scanKeys(pattern);
219
- if (keys.length === 0) {
220
- return {
221
- threads: [],
222
- total: 0,
223
- page,
224
- perPage: perPageForResponse,
225
- hasMore: false
226
- };
227
- }
228
- const results = await this.client.mGet(keys);
229
- for (let i = 0; i < results.length; i++) {
230
- const data = results[i];
231
- if (!data) {
232
- continue;
233
- }
234
- const thread = JSON.parse(data);
235
- if (filter?.resourceId && thread.resourceId !== filter.resourceId) {
236
- continue;
237
- }
238
- if (filter?.metadata && Object.keys(filter.metadata).length > 0) {
239
- const threadMetadata = typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata;
240
- const matches = Object.entries(filter.metadata).every(
241
- ([key, value]) => jsonValueEquals(threadMetadata?.[key], value)
242
- );
243
- if (!matches) {
244
- continue;
245
- }
246
- }
247
- allThreads.push({
248
- ...thread,
249
- createdAt: ensureDate(thread.createdAt),
250
- updatedAt: ensureDate(thread.updatedAt),
251
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata
252
- });
253
- }
254
- const sortedThreads = this.sortThreads(allThreads, field, direction);
255
- const total = sortedThreads.length;
256
- const end = perPageInput === false ? total : offset + perPage;
257
- const paginatedThreads = sortedThreads.slice(offset, end);
258
- const hasMore = perPageInput === false ? false : end < total;
259
- return {
260
- threads: paginatedThreads,
261
- total,
262
- page,
263
- perPage: perPageForResponse,
264
- hasMore
265
- };
266
- } catch (error) {
267
- const mastraError = new MastraError(
268
- {
269
- id: createStorageErrorId("REDIS", "LIST_THREADS", "FAILED"),
270
- domain: ErrorDomain.STORAGE,
271
- category: ErrorCategory.THIRD_PARTY,
272
- details: {
273
- ...filter?.resourceId && { resourceId: filter.resourceId },
274
- hasMetadataFilter: !!filter?.metadata,
275
- page,
276
- perPage
277
- }
278
- },
279
- error
280
- );
281
- this.logger.trackException(mastraError);
282
- this.logger.error(mastraError.toString());
283
- return {
284
- threads: [],
285
- total: 0,
286
- page,
287
- perPage: perPageForResponse,
288
- hasMore: false
289
- };
290
- }
291
- }
292
- async saveThread({ thread }) {
293
- try {
294
- await this.db.insert({
295
- tableName: TABLE_THREADS,
296
- record: thread
297
- });
298
- return thread;
299
- } catch (error) {
300
- const mastraError = new MastraError(
301
- {
302
- id: createStorageErrorId("REDIS", "SAVE_THREAD", "FAILED"),
303
- domain: ErrorDomain.STORAGE,
304
- category: ErrorCategory.THIRD_PARTY,
305
- details: {
306
- threadId: thread.id
307
- }
308
- },
309
- error
310
- );
311
- this.logger.trackException(mastraError);
312
- this.logger.error(mastraError.toString());
313
- throw mastraError;
314
- }
315
- }
316
- async updateThread({
317
- id,
318
- title,
319
- metadata
320
- }) {
321
- const thread = await this.getThreadById({ threadId: id });
322
- if (!thread) {
323
- throw new MastraError({
324
- id: createStorageErrorId("REDIS", "UPDATE_THREAD", "FAILED"),
325
- domain: ErrorDomain.STORAGE,
326
- category: ErrorCategory.USER,
327
- text: `Thread ${id} not found`,
328
- details: {
329
- threadId: id
330
- }
331
- });
332
- }
333
- const updatedThread = {
334
- ...thread,
335
- title,
336
- metadata: {
337
- ...thread.metadata,
338
- ...metadata
339
- },
340
- updatedAt: /* @__PURE__ */ new Date()
341
- };
342
- try {
343
- await this.saveThread({ thread: updatedThread });
344
- return updatedThread;
345
- } catch (error) {
346
- throw new MastraError(
347
- {
348
- id: createStorageErrorId("REDIS", "UPDATE_THREAD", "FAILED"),
349
- domain: ErrorDomain.STORAGE,
350
- category: ErrorCategory.THIRD_PARTY,
351
- details: {
352
- threadId: id
353
- }
354
- },
355
- error
356
- );
357
- }
358
- }
359
- async deleteThread({ threadId }) {
360
- const threadKey = getKey(TABLE_THREADS, { id: threadId });
361
- const threadMessagesKey = getThreadMessagesKey(threadId);
362
- try {
363
- const messageIds = await this.client.zRange(threadMessagesKey, 0, -1);
364
- const multi = this.client.multi();
365
- multi.del(threadKey);
366
- multi.del(threadMessagesKey);
367
- for (const messageId of messageIds) {
368
- const messageKey = getMessageKey(threadId, messageId);
369
- multi.del(messageKey);
370
- multi.del(getMessageIndexKey(messageId));
371
- }
372
- await multi.exec();
373
- await this.db.scanAndDelete(getMessageKey(threadId, "*"));
374
- } catch (error) {
375
- throw new MastraError(
376
- {
377
- id: createStorageErrorId("REDIS", "DELETE_THREAD", "FAILED"),
378
- domain: ErrorDomain.STORAGE,
379
- category: ErrorCategory.THIRD_PARTY,
380
- details: {
381
- threadId
382
- }
383
- },
384
- error
385
- );
386
- }
387
- }
388
- async saveMessages(args) {
389
- const { messages } = args;
390
- if (messages.length === 0) {
391
- return { messages: [] };
392
- }
393
- const threadId = messages[0]?.threadId;
394
- try {
395
- if (!threadId) {
396
- throw new Error("Thread ID is required");
397
- }
398
- const thread = await this.getThreadById({ threadId });
399
- if (!thread) {
400
- throw new Error(`Thread ${threadId} not found`);
401
- }
402
- } catch (error) {
403
- throw new MastraError(
404
- {
405
- id: createStorageErrorId("REDIS", "SAVE_MESSAGES", "INVALID_ARGS"),
406
- domain: ErrorDomain.STORAGE,
407
- category: ErrorCategory.USER
408
- },
409
- error
410
- );
411
- }
412
- const messagesWithIndex = messages.map((message, index) => {
413
- if (!message.threadId) {
414
- throw new Error(
415
- `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`
416
- );
417
- }
418
- if (!message.resourceId) {
419
- throw new Error(
420
- `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`
421
- );
422
- }
423
- return {
424
- ...message,
425
- _index: index
426
- };
427
- });
428
- const threadKey = getKey(TABLE_THREADS, { id: threadId });
429
- const existingThreadData = await this.client.get(threadKey);
430
- const existingThread = existingThreadData ? JSON.parse(existingThreadData) : null;
431
- try {
432
- const batchSize = 1e3;
433
- const existingThreadIds = await this.client.mGet(
434
- messagesWithIndex.map((message) => getMessageIndexKey(message.id))
435
- );
436
- for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
437
- const batch = messagesWithIndex.slice(i, i + batchSize);
438
- const batchExistingThreadIds = existingThreadIds.slice(i, i + batch.length);
439
- const multi = this.client.multi();
440
- for (const [batchIndex, message] of batch.entries()) {
441
- const key = getMessageKey(message.threadId, message.id);
442
- const score = getMessageScore(message);
443
- const existingThreadId = batchExistingThreadIds[batchIndex];
444
- if (existingThreadId && existingThreadId !== message.threadId) {
445
- const existingMessageKey = getMessageKey(existingThreadId, message.id);
446
- multi.del(existingMessageKey);
447
- multi.zRem(getThreadMessagesKey(existingThreadId), message.id);
448
- }
449
- multi.set(key, JSON.stringify(message));
450
- multi.set(getMessageIndexKey(message.id), message.threadId);
451
- multi.zAdd(getThreadMessagesKey(message.threadId), { score, value: message.id });
452
- }
453
- if (i === 0 && existingThread) {
454
- const updatedThread = {
455
- ...existingThread,
456
- updatedAt: /* @__PURE__ */ new Date()
457
- };
458
- multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, updatedThread).processedRecord));
459
- }
460
- await multi.exec();
461
- }
462
- const list = new MessageList().add(messages, "memory");
463
- return { messages: list.get.all.db() };
464
- } catch (error) {
465
- throw new MastraError(
466
- {
467
- id: createStorageErrorId("REDIS", "SAVE_MESSAGES", "FAILED"),
468
- domain: ErrorDomain.STORAGE,
469
- category: ErrorCategory.THIRD_PARTY,
470
- details: {
471
- threadId
472
- }
473
- },
474
- error
475
- );
476
- }
477
- }
478
- async getThreadIdForMessage(messageId) {
479
- const indexedThreadId = await this.client.get(getMessageIndexKey(messageId));
480
- if (indexedThreadId) {
481
- return indexedThreadId;
482
- }
483
- const keys = await this.db.scanKeys(getMessageKey("*", messageId));
484
- if (keys.length === 0) {
485
- return null;
486
- }
487
- const messageData = await this.client.get(keys[0]);
488
- if (!messageData) {
489
- return null;
490
- }
491
- const message = JSON.parse(messageData);
492
- if (message.threadId) {
493
- await this.client.set(getMessageIndexKey(messageId), message.threadId);
494
- }
495
- return message.threadId || null;
496
- }
497
- async getIncludedMessages(include) {
498
- if (!include?.length) {
499
- return [];
500
- }
501
- const messageIds = /* @__PURE__ */ new Set();
502
- const messageIdToThreadIds = {};
503
- for (const item of include) {
504
- const itemThreadId = await this.getThreadIdForMessage(item.id);
505
- if (!itemThreadId) {
506
- continue;
507
- }
508
- messageIds.add(item.id);
509
- messageIdToThreadIds[item.id] = itemThreadId;
510
- const itemThreadMessagesKey = getThreadMessagesKey(itemThreadId);
511
- const rank = await this.client.zRank(itemThreadMessagesKey, item.id);
512
- if (rank === null) {
513
- continue;
514
- }
515
- if (item.withPreviousMessages) {
516
- const start = Math.max(0, rank - item.withPreviousMessages);
517
- const prevIds = rank === 0 ? [] : await this.client.zRange(itemThreadMessagesKey, start, rank - 1);
518
- prevIds.forEach((id) => {
519
- messageIds.add(id);
520
- messageIdToThreadIds[id] = itemThreadId;
521
- });
522
- }
523
- if (item.withNextMessages) {
524
- const nextIds = await this.client.zRange(itemThreadMessagesKey, rank + 1, rank + item.withNextMessages);
525
- nextIds.forEach((id) => {
526
- messageIds.add(id);
527
- messageIdToThreadIds[id] = itemThreadId;
528
- });
529
- }
530
- }
531
- if (messageIds.size === 0) {
532
- return [];
533
- }
534
- const keysToFetch = Array.from(messageIds).map((id) => getMessageKey(messageIdToThreadIds[id], id));
535
- const results = await this.client.mGet(keysToFetch);
536
- return results.filter((data) => data !== null).map((data) => JSON.parse(data));
537
- }
538
- parseStoredMessage(storedMessage) {
539
- const defaultMessageContent = { format: 2, parts: [{ type: "text", text: "" }] };
540
- const { _index, ...rest } = storedMessage;
541
- return {
542
- ...rest,
543
- createdAt: new Date(rest.createdAt),
544
- content: rest.content || defaultMessageContent
545
- };
546
- }
547
- async listMessagesById({ messageIds }) {
548
- if (messageIds.length === 0) {
549
- return { messages: [] };
550
- }
551
- try {
552
- const rawMessages = [];
553
- const indexKeys = messageIds.map((id) => getMessageIndexKey(id));
554
- const indexResults = await this.client.mGet(indexKeys);
555
- const indexedIds = [];
556
- const unindexedIds = [];
557
- messageIds.forEach((id, i) => {
558
- const threadId = indexResults[i];
559
- if (threadId) {
560
- indexedIds.push({ messageId: id, threadId });
561
- return;
562
- }
563
- unindexedIds.push(id);
564
- });
565
- if (indexedIds.length > 0) {
566
- const messageKeys = indexedIds.map(({ messageId, threadId }) => getMessageKey(threadId, messageId));
567
- const messageResults = await this.client.mGet(messageKeys);
568
- for (const data of messageResults) {
569
- if (data) {
570
- rawMessages.push(JSON.parse(data));
571
- }
572
- }
573
- }
574
- if (unindexedIds.length > 0) {
575
- const threadKeys = await this.db.scanKeys("thread:*:messages");
576
- const result = await Promise.all(
577
- threadKeys.map(async (threadKey) => {
578
- const threadId = threadKey.split(":")[1];
579
- if (!threadId) {
580
- throw new Error(`Failed to parse thread ID from thread key "${threadKey}"`);
581
- }
582
- const msgKeys = unindexedIds.map((id) => getMessageKey(threadId, id));
583
- return this.client.mGet(msgKeys);
584
- })
585
- );
586
- const foundMessages = result.flat(1).filter((data) => !!data).map((data) => JSON.parse(data));
587
- rawMessages.push(...foundMessages);
588
- if (foundMessages.length > 0) {
589
- const multi = this.client.multi();
590
- foundMessages.forEach((msg) => {
591
- if (msg.threadId) {
592
- multi.set(getMessageIndexKey(msg.id), msg.threadId);
593
- }
594
- });
595
- await multi.exec();
596
- }
597
- }
598
- const list = new MessageList().add(rawMessages.map(this.parseStoredMessage), "memory");
599
- return { messages: list.get.all.db() };
600
- } catch (error) {
601
- throw new MastraError(
602
- {
603
- id: createStorageErrorId("REDIS", "LIST_MESSAGES_BY_ID", "FAILED"),
604
- domain: ErrorDomain.STORAGE,
605
- category: ErrorCategory.THIRD_PARTY,
606
- details: {
607
- messageIds: JSON.stringify(messageIds)
608
- }
609
- },
610
- error
611
- );
612
- }
613
- }
614
- async listMessages(args) {
615
- const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
616
- const threadIds = Array.isArray(threadId) ? threadId : [threadId];
617
- const threadIdsSet = new Set(threadIds);
618
- if (threadIds.length === 0 || threadIds.some((id) => !id.trim())) {
619
- throw new MastraError(
620
- {
621
- id: createStorageErrorId("REDIS", "LIST_MESSAGES", "INVALID_THREAD_ID"),
622
- domain: ErrorDomain.STORAGE,
623
- category: ErrorCategory.USER,
624
- details: { threadId: Array.isArray(threadId) ? threadId.join(",") : threadId }
625
- },
626
- new Error("threadId must be a non-empty string or array of non-empty strings")
627
- );
628
- }
629
- const perPage = normalizePerPage(perPageInput, 40);
630
- const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
631
- try {
632
- if (page < 0) {
633
- throw new MastraError(
634
- {
635
- id: createStorageErrorId("REDIS", "LIST_MESSAGES", "INVALID_PAGE"),
636
- domain: ErrorDomain.STORAGE,
637
- category: ErrorCategory.USER,
638
- details: { page }
639
- },
640
- new Error("page must be >= 0")
641
- );
642
- }
643
- const { field, direction } = this.parseOrderBy(orderBy, "ASC");
644
- const getFieldValue = (msg) => {
645
- if (field === "createdAt") {
646
- return new Date(msg.createdAt).getTime();
647
- }
648
- const value = msg[field];
649
- if (typeof value === "number") {
650
- return value;
651
- }
652
- if (value instanceof Date) {
653
- return value.getTime();
654
- }
655
- return 0;
656
- };
657
- if (perPage === 0 && (!include || include.length === 0)) {
658
- return {
659
- messages: [],
660
- total: 0,
661
- page,
662
- perPage: perPageForResponse,
663
- hasMore: false
664
- };
665
- }
666
- let includedMessages = [];
667
- if (include && include.length > 0) {
668
- const included = await this.getIncludedMessages(include);
669
- includedMessages = included.map(this.parseStoredMessage);
670
- }
671
- if (perPage === 0 && include && include.length > 0) {
672
- const list2 = new MessageList().add(includedMessages, "memory");
673
- const messages = list2.get.all.db().sort((a, b) => {
674
- const aValue = getFieldValue(a);
675
- const bValue = getFieldValue(b);
676
- return direction === "ASC" ? aValue - bValue : bValue - aValue;
677
- });
678
- return {
679
- messages,
680
- total: 0,
681
- page,
682
- perPage: perPageForResponse,
683
- hasMore: false
684
- };
685
- }
686
- const allMessageIdsWithThreads = [];
687
- for (const tid of threadIds) {
688
- const threadMessagesKey = getThreadMessagesKey(tid);
689
- const msgIds = await this.client.zRange(threadMessagesKey, 0, -1);
690
- for (const mid of msgIds) {
691
- allMessageIdsWithThreads.push({ threadId: tid, messageId: mid });
692
- }
693
- }
694
- if (allMessageIdsWithThreads.length === 0) {
695
- return {
696
- messages: [],
697
- total: 0,
698
- page,
699
- perPage: perPageForResponse,
700
- hasMore: false
701
- };
702
- }
703
- const messageKeys = allMessageIdsWithThreads.map(({ threadId: tid, messageId }) => getMessageKey(tid, messageId));
704
- const results = await this.client.mGet(messageKeys);
705
- let messagesData = results.filter((data) => data !== null).map((data) => JSON.parse(data)).map(this.parseStoredMessage);
706
- if (resourceId) {
707
- messagesData = messagesData.filter((msg) => msg.resourceId === resourceId);
708
- }
709
- messagesData = filterByDateRange(
710
- messagesData,
711
- (msg) => new Date(msg.createdAt),
712
- filter?.dateRange
713
- );
714
- messagesData.sort((a, b) => {
715
- const aValue = getFieldValue(a);
716
- const bValue = getFieldValue(b);
717
- return direction === "ASC" ? aValue - bValue : bValue - aValue;
718
- });
719
- const total = messagesData.length;
720
- const start = offset;
721
- const end = perPageInput === false ? total : start + perPage;
722
- const paginatedMessages = messagesData.slice(start, end);
723
- const messageIdsSet = /* @__PURE__ */ new Set();
724
- const allMessages = [];
725
- for (const msg of paginatedMessages) {
726
- if (messageIdsSet.has(msg.id)) {
727
- continue;
728
- }
729
- allMessages.push(msg);
730
- messageIdsSet.add(msg.id);
731
- }
732
- for (const msg of includedMessages) {
733
- if (messageIdsSet.has(msg.id)) {
734
- continue;
735
- }
736
- allMessages.push(msg);
737
- messageIdsSet.add(msg.id);
738
- }
739
- const list = new MessageList().add(allMessages, "memory");
740
- let finalMessages = list.get.all.db();
741
- finalMessages = finalMessages.sort((a, b) => {
742
- const aValue = getFieldValue(a);
743
- const bValue = getFieldValue(b);
744
- return direction === "ASC" ? aValue - bValue : bValue - aValue;
745
- });
746
- const returnedThreadMessageIds = new Set(
747
- finalMessages.filter((m) => {
748
- return m.threadId && threadIdsSet.has(m.threadId);
749
- }).map((m) => m.id)
750
- );
751
- const allThreadMessagesReturned = returnedThreadMessageIds.size >= total;
752
- const hasMore = perPageInput !== false && !allThreadMessagesReturned && end < total;
753
- return {
754
- messages: finalMessages,
755
- total,
756
- page,
757
- perPage: perPageForResponse,
758
- hasMore
759
- };
760
- } catch (error) {
761
- const mastraError = new MastraError(
762
- {
763
- id: createStorageErrorId("REDIS", "LIST_MESSAGES", "FAILED"),
764
- domain: ErrorDomain.STORAGE,
765
- category: ErrorCategory.THIRD_PARTY,
766
- details: {
767
- threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
768
- resourceId: resourceId ?? ""
769
- }
770
- },
771
- error
772
- );
773
- this.logger.error(mastraError.toString());
774
- this.logger.trackException(mastraError);
775
- return {
776
- messages: [],
777
- total: 0,
778
- page,
779
- perPage: perPageForResponse,
780
- hasMore: false
781
- };
782
- }
783
- }
784
- async getResourceById({ resourceId }) {
785
- try {
786
- const key = `${TABLE_RESOURCES}:${resourceId}`;
787
- const data = await this.client.get(key);
788
- if (!data) {
789
- return null;
790
- }
791
- const resource = JSON.parse(data);
792
- return {
793
- ...resource,
794
- createdAt: new Date(resource.createdAt),
795
- updatedAt: new Date(resource.updatedAt),
796
- workingMemory: typeof resource.workingMemory === "object" ? JSON.stringify(resource.workingMemory) : resource.workingMemory,
797
- metadata: typeof resource.metadata === "string" ? JSON.parse(resource.metadata) : resource.metadata
798
- };
799
- } catch (error) {
800
- this.logger.error("Error getting resource by ID:", error);
801
- throw error;
802
- }
803
- }
804
- async saveResource({ resource }) {
805
- try {
806
- const key = `${TABLE_RESOURCES}:${resource.id}`;
807
- const serializedResource = {
808
- ...resource,
809
- metadata: JSON.stringify(resource.metadata),
810
- createdAt: resource.createdAt.toISOString(),
811
- updatedAt: resource.updatedAt.toISOString()
812
- };
813
- await this.client.set(key, JSON.stringify(serializedResource));
814
- return resource;
815
- } catch (error) {
816
- this.logger.error("Error saving resource:", error);
817
- throw error;
818
- }
819
- }
820
- async updateResource({
821
- resourceId,
822
- workingMemory,
823
- metadata
824
- }) {
825
- try {
826
- const existingResource = await this.getResourceById({ resourceId });
827
- if (!existingResource) {
828
- const newResource = {
829
- id: resourceId,
830
- workingMemory,
831
- metadata: metadata || {},
832
- createdAt: /* @__PURE__ */ new Date(),
833
- updatedAt: /* @__PURE__ */ new Date()
834
- };
835
- return this.saveResource({ resource: newResource });
836
- }
837
- const updatedResource = {
838
- ...existingResource,
839
- workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
840
- metadata: {
841
- ...existingResource.metadata,
842
- ...metadata
843
- },
844
- updatedAt: /* @__PURE__ */ new Date()
845
- };
846
- await this.saveResource({ resource: updatedResource });
847
- return updatedResource;
848
- } catch (error) {
849
- this.logger.error("Error updating resource:", error);
850
- throw error;
851
- }
852
- }
853
- async updateMessages(args) {
854
- const { messages } = args;
855
- if (messages.length === 0) {
856
- return [];
857
- }
858
- try {
859
- const messageIds = messages.map((m) => m.id);
860
- const existingMessages = [];
861
- const messageIdToKey = {};
862
- for (const messageId of messageIds) {
863
- const pattern = getMessageKey("*", messageId);
864
- const keys = await this.db.scanKeys(pattern);
865
- for (const key of keys) {
866
- const data = await this.client.get(key);
867
- if (!data) {
868
- continue;
869
- }
870
- const message = JSON.parse(data);
871
- if (message && message.id === messageId) {
872
- existingMessages.push(message);
873
- messageIdToKey[messageId] = key;
874
- break;
875
- }
876
- }
877
- }
878
- if (existingMessages.length === 0) {
879
- return [];
880
- }
881
- const threadIdsToUpdate = /* @__PURE__ */ new Set();
882
- const multi = this.client.multi();
883
- for (const existingMessage of existingMessages) {
884
- const updatePayload = messages.find((m) => m.id === existingMessage.id);
885
- if (!updatePayload) {
886
- continue;
887
- }
888
- const { id, ...fieldsToUpdate } = updatePayload;
889
- if (Object.keys(fieldsToUpdate).length === 0) {
890
- continue;
891
- }
892
- threadIdsToUpdate.add(existingMessage.threadId);
893
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
894
- threadIdsToUpdate.add(updatePayload.threadId);
895
- }
896
- const updatedMessage = { ...existingMessage };
897
- if (fieldsToUpdate.content) {
898
- const existingContent = existingMessage.content;
899
- const newContent = {
900
- ...existingContent,
901
- ...fieldsToUpdate.content,
902
- ...existingContent?.metadata && fieldsToUpdate.content.metadata ? {
903
- metadata: {
904
- ...existingContent.metadata,
905
- ...fieldsToUpdate.content.metadata
906
- }
907
- } : {}
908
- };
909
- updatedMessage.content = newContent;
910
- }
911
- for (const key2 in fieldsToUpdate) {
912
- if (Object.prototype.hasOwnProperty.call(fieldsToUpdate, key2) && key2 !== "content") {
913
- updatedMessage[key2] = fieldsToUpdate[key2];
914
- }
915
- }
916
- const key = messageIdToKey[id];
917
- if (!key) {
918
- continue;
919
- }
920
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
921
- multi.zRem(getThreadMessagesKey(existingMessage.threadId), id);
922
- multi.del(key);
923
- const newKey = getMessageKey(updatePayload.threadId, id);
924
- multi.set(newKey, JSON.stringify(updatedMessage));
925
- multi.set(getMessageIndexKey(id), updatePayload.threadId);
926
- const score = getMessageScore(updatedMessage);
927
- multi.zAdd(getThreadMessagesKey(updatePayload.threadId), { score, value: id });
928
- messageIdToKey[id] = newKey;
929
- continue;
930
- }
931
- multi.set(key, JSON.stringify(updatedMessage));
932
- }
933
- const now = /* @__PURE__ */ new Date();
934
- for (const threadId of threadIdsToUpdate) {
935
- if (threadId) {
936
- const threadKey = getKey(TABLE_THREADS, { id: threadId });
937
- const existingThreadData = await this.client.get(threadKey);
938
- if (existingThreadData) {
939
- const existingThread = JSON.parse(existingThreadData);
940
- const updatedThread = {
941
- ...existingThread,
942
- updatedAt: now
943
- };
944
- multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, updatedThread).processedRecord));
945
- }
946
- }
947
- }
948
- await multi.exec();
949
- const updatedMessages = [];
950
- for (const messageId of messageIds) {
951
- const key = messageIdToKey[messageId];
952
- if (key) {
953
- const data = await this.client.get(key);
954
- if (data) {
955
- updatedMessages.push(JSON.parse(data));
956
- }
957
- }
958
- }
959
- return updatedMessages;
960
- } catch (error) {
961
- throw new MastraError(
962
- {
963
- id: createStorageErrorId("REDIS", "UPDATE_MESSAGES", "FAILED"),
964
- domain: ErrorDomain.STORAGE,
965
- category: ErrorCategory.THIRD_PARTY,
966
- details: {
967
- messageIds: messages.map((m) => m.id).join(",")
968
- }
969
- },
970
- error
971
- );
972
- }
973
- }
974
- async deleteMessages(messageIds) {
975
- if (!messageIds || messageIds.length === 0) {
976
- return;
977
- }
978
- try {
979
- const threadIds = /* @__PURE__ */ new Set();
980
- const messageKeys = [];
981
- const foundMessageIds = [];
982
- const messageIdToThreadId = /* @__PURE__ */ new Map();
983
- const indexKeys = messageIds.map((id) => getMessageIndexKey(id));
984
- const indexResults = await this.client.mGet(indexKeys);
985
- const indexedMessages = [];
986
- const unindexedMessageIds = [];
987
- messageIds.forEach((id, i) => {
988
- const threadId = indexResults[i];
989
- if (threadId) {
990
- indexedMessages.push({ messageId: id, threadId });
991
- return;
992
- }
993
- unindexedMessageIds.push(id);
994
- });
995
- for (const { messageId, threadId } of indexedMessages) {
996
- messageKeys.push(getMessageKey(threadId, messageId));
997
- foundMessageIds.push(messageId);
998
- messageIdToThreadId.set(messageId, threadId);
999
- threadIds.add(threadId);
1000
- }
1001
- for (const messageId of unindexedMessageIds) {
1002
- const pattern = getMessageKey("*", messageId);
1003
- const keys = await this.db.scanKeys(pattern);
1004
- for (const key of keys) {
1005
- const data = await this.client.get(key);
1006
- if (!data) {
1007
- continue;
1008
- }
1009
- const message = JSON.parse(data);
1010
- if (message && message.id === messageId) {
1011
- messageKeys.push(key);
1012
- foundMessageIds.push(messageId);
1013
- if (message.threadId) {
1014
- messageIdToThreadId.set(messageId, message.threadId);
1015
- threadIds.add(message.threadId);
1016
- }
1017
- break;
1018
- }
1019
- }
1020
- }
1021
- if (messageKeys.length === 0) {
1022
- return;
1023
- }
1024
- const multi = this.client.multi();
1025
- for (const key of messageKeys) {
1026
- multi.del(key);
1027
- }
1028
- for (const messageId of foundMessageIds) {
1029
- multi.del(getMessageIndexKey(messageId));
1030
- }
1031
- if (threadIds.size > 0) {
1032
- for (const threadId of threadIds) {
1033
- for (const [msgId, msgThreadId] of messageIdToThreadId) {
1034
- if (msgThreadId === threadId) {
1035
- multi.zRem(getThreadMessagesKey(threadId), msgId);
1036
- }
1037
- }
1038
- const threadKey = getKey(TABLE_THREADS, { id: threadId });
1039
- const threadData = await this.client.get(threadKey);
1040
- if (!threadData) {
1041
- continue;
1042
- }
1043
- const thread = JSON.parse(threadData);
1044
- const updatedThread = { ...thread, updatedAt: /* @__PURE__ */ new Date() };
1045
- multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, updatedThread).processedRecord));
1046
- }
1047
- }
1048
- await multi.exec();
1049
- } catch (error) {
1050
- throw new MastraError(
1051
- {
1052
- id: createStorageErrorId("REDIS", "DELETE_MESSAGES", "FAILED"),
1053
- domain: ErrorDomain.STORAGE,
1054
- category: ErrorCategory.THIRD_PARTY,
1055
- details: { messageIds: messageIds.join(", ") }
1056
- },
1057
- error
1058
- );
1059
- }
1060
- }
1061
- sortThreads(threads, field, direction) {
1062
- return threads.sort((a, b) => {
1063
- const aValue = new Date(a[field]).getTime();
1064
- const bValue = new Date(b[field]).getTime();
1065
- return direction === "ASC" ? aValue - bValue : bValue - aValue;
1066
- });
1067
- }
1068
- async cloneThread(args) {
1069
- const { sourceThreadId, newThreadId: providedThreadId, resourceId, title, metadata, options } = args;
1070
- const sourceThread = await this.getThreadById({ threadId: sourceThreadId });
1071
- if (!sourceThread) {
1072
- throw new MastraError({
1073
- id: createStorageErrorId("REDIS", "CLONE_THREAD", "SOURCE_NOT_FOUND"),
1074
- domain: ErrorDomain.STORAGE,
1075
- category: ErrorCategory.USER,
1076
- text: `Source thread with id ${sourceThreadId} not found`,
1077
- details: { sourceThreadId }
1078
- });
1079
- }
1080
- const newThreadId = providedThreadId || crypto.randomUUID();
1081
- const existingThread = await this.getThreadById({ threadId: newThreadId });
1082
- if (existingThread) {
1083
- throw new MastraError({
1084
- id: createStorageErrorId("REDIS", "CLONE_THREAD", "THREAD_EXISTS"),
1085
- domain: ErrorDomain.STORAGE,
1086
- category: ErrorCategory.USER,
1087
- text: `Thread with id ${newThreadId} already exists`,
1088
- details: { newThreadId }
1089
- });
1090
- }
1091
- try {
1092
- const threadMessagesKey = getThreadMessagesKey(sourceThreadId);
1093
- const msgIds = await this.client.zRange(threadMessagesKey, 0, -1);
1094
- const messageKeys = msgIds.map((mid) => getMessageKey(sourceThreadId, mid));
1095
- let sourceMessages = [];
1096
- if (messageKeys.length > 0) {
1097
- const results = await this.client.mGet(messageKeys);
1098
- sourceMessages = results.filter((data) => data !== null).map((data) => {
1099
- const msg = JSON.parse(data);
1100
- return { ...msg, createdAt: new Date(msg.createdAt) };
1101
- });
1102
- }
1103
- if (options?.messageFilter?.startDate || options?.messageFilter?.endDate) {
1104
- sourceMessages = filterByDateRange(sourceMessages, (msg) => new Date(msg.createdAt), {
1105
- start: options.messageFilter?.startDate,
1106
- end: options.messageFilter?.endDate
1107
- });
1108
- }
1109
- if (options?.messageFilter?.messageIds && options.messageFilter.messageIds.length > 0) {
1110
- const messageIdSet = new Set(options.messageFilter.messageIds);
1111
- sourceMessages = sourceMessages.filter((msg) => messageIdSet.has(msg.id));
1112
- }
1113
- sourceMessages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
1114
- if (options?.messageLimit && options.messageLimit > 0 && sourceMessages.length > options.messageLimit) {
1115
- sourceMessages = sourceMessages.slice(-options.messageLimit);
1116
- }
1117
- const now = /* @__PURE__ */ new Date();
1118
- const lastMessageId = sourceMessages.length > 0 ? sourceMessages[sourceMessages.length - 1].id : void 0;
1119
- const cloneMetadata = {
1120
- sourceThreadId,
1121
- clonedAt: now,
1122
- ...lastMessageId && { lastMessageId }
1123
- };
1124
- const newThread = {
1125
- id: newThreadId,
1126
- resourceId: resourceId || sourceThread.resourceId,
1127
- title: title || (sourceThread.title ? `Clone of ${sourceThread.title}` : void 0),
1128
- metadata: { ...metadata, clone: cloneMetadata },
1129
- createdAt: now,
1130
- updatedAt: now
1131
- };
1132
- const multi = this.client.multi();
1133
- const threadKey = getKey(TABLE_THREADS, { id: newThreadId });
1134
- multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, newThread).processedRecord));
1135
- const clonedMessages = [];
1136
- const targetResourceId = resourceId || sourceThread.resourceId;
1137
- const newThreadMessagesKey = getThreadMessagesKey(newThreadId);
1138
- for (let i = 0; i < sourceMessages.length; i++) {
1139
- const sourceMsg = sourceMessages[i];
1140
- const newMessageId = crypto.randomUUID();
1141
- const { _index, ...restMsg } = sourceMsg;
1142
- const newMessage = {
1143
- ...restMsg,
1144
- id: newMessageId,
1145
- threadId: newThreadId,
1146
- resourceId: targetResourceId
1147
- };
1148
- const messageKey = getMessageKey(newThreadId, newMessageId);
1149
- multi.set(messageKey, JSON.stringify(newMessage));
1150
- multi.set(getMessageIndexKey(newMessageId), newThreadId);
1151
- const score = getMessageScore({ createdAt: newMessage.createdAt, _index: i });
1152
- multi.zAdd(newThreadMessagesKey, { score, value: newMessageId });
1153
- clonedMessages.push(newMessage);
1154
- }
1155
- await multi.exec();
1156
- return {
1157
- thread: newThread,
1158
- clonedMessages
1159
- };
1160
- } catch (error) {
1161
- if (error instanceof MastraError) {
1162
- throw error;
1163
- }
1164
- throw new MastraError(
1165
- {
1166
- id: createStorageErrorId("REDIS", "CLONE_THREAD", "FAILED"),
1167
- domain: ErrorDomain.STORAGE,
1168
- category: ErrorCategory.THIRD_PARTY,
1169
- details: { sourceThreadId, newThreadId }
1170
- },
1171
- error
1172
- );
1173
- }
1174
- }
137
+ client;
138
+ db;
139
+ constructor(config) {
140
+ super();
141
+ this.client = config.client;
142
+ this.db = new RedisDB({ client: config.client });
143
+ }
144
+ async dangerouslyClearAll() {
145
+ await this.db.deleteData({ tableName: TABLE_THREADS });
146
+ await this.db.deleteData({ tableName: TABLE_MESSAGES });
147
+ await this.db.deleteData({ tableName: TABLE_RESOURCES });
148
+ await this.db.scanAndDelete("msg-idx:*");
149
+ await this.db.scanAndDelete("thread:*:messages");
150
+ }
151
+ async getThreadById({ threadId, resourceId }) {
152
+ try {
153
+ const thread = await this.db.get({
154
+ tableName: TABLE_THREADS,
155
+ keys: { id: threadId }
156
+ });
157
+ if (!thread || resourceId !== void 0 && thread.resourceId !== resourceId) return null;
158
+ return {
159
+ ...thread,
160
+ createdAt: ensureDate(thread.createdAt),
161
+ updatedAt: ensureDate(thread.updatedAt),
162
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata
163
+ };
164
+ } catch (error) {
165
+ throw new MastraError({
166
+ id: createStorageErrorId("REDIS", "GET_THREAD_BY_ID", "FAILED"),
167
+ domain: ErrorDomain.STORAGE,
168
+ category: ErrorCategory.THIRD_PARTY,
169
+ details: { threadId }
170
+ }, error);
171
+ }
172
+ }
173
+ async listThreadsByResourceId(args) {
174
+ return this.listThreads(args);
175
+ }
176
+ async listThreads(args) {
177
+ const { page = 0, perPage: perPageInput, orderBy, filter } = args;
178
+ const { field, direction } = this.parseOrderBy(orderBy);
179
+ try {
180
+ this.validatePaginationInput(page, perPageInput ?? 100);
181
+ } catch (error) {
182
+ throw new MastraError({
183
+ id: createStorageErrorId("REDIS", "LIST_THREADS", "INVALID_PAGE"),
184
+ domain: ErrorDomain.STORAGE,
185
+ category: ErrorCategory.USER,
186
+ details: {
187
+ page,
188
+ ...perPageInput !== void 0 && { perPage: perPageInput }
189
+ }
190
+ }, error instanceof Error ? error : /* @__PURE__ */ new Error("Invalid pagination parameters"));
191
+ }
192
+ const perPage = normalizePerPage(perPageInput, 100);
193
+ try {
194
+ this.validateMetadataKeys(filter?.metadata);
195
+ } catch (error) {
196
+ throw new MastraError({
197
+ id: createStorageErrorId("REDIS", "LIST_THREADS", "INVALID_METADATA_KEY"),
198
+ domain: ErrorDomain.STORAGE,
199
+ category: ErrorCategory.USER,
200
+ details: { metadataKeys: filter?.metadata ? Object.keys(filter.metadata).join(", ") : "" }
201
+ }, error instanceof Error ? error : /* @__PURE__ */ new Error("Invalid metadata key"));
202
+ }
203
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
204
+ try {
205
+ let allThreads = [];
206
+ const pattern = `${TABLE_THREADS}:*`;
207
+ const keys = await this.db.scanKeys(pattern);
208
+ if (keys.length === 0) return {
209
+ threads: [],
210
+ total: 0,
211
+ page,
212
+ perPage: perPageForResponse,
213
+ hasMore: false
214
+ };
215
+ const results = await this.client.mGet(keys);
216
+ for (let i = 0; i < results.length; i++) {
217
+ const data = results[i];
218
+ if (!data) continue;
219
+ const thread = JSON.parse(data);
220
+ if (filter?.resourceId && thread.resourceId !== filter.resourceId) continue;
221
+ if (filter?.metadata && Object.keys(filter.metadata).length > 0) {
222
+ const threadMetadata = typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata;
223
+ if (!Object.entries(filter.metadata).every(([key, value]) => jsonValueEquals(threadMetadata?.[key], value))) continue;
224
+ }
225
+ allThreads.push({
226
+ ...thread,
227
+ createdAt: ensureDate(thread.createdAt),
228
+ updatedAt: ensureDate(thread.updatedAt),
229
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata
230
+ });
231
+ }
232
+ const sortedThreads = this.sortThreads(allThreads, field, direction);
233
+ const total = sortedThreads.length;
234
+ const end = perPageInput === false ? total : offset + perPage;
235
+ return {
236
+ threads: sortedThreads.slice(offset, end),
237
+ total,
238
+ page,
239
+ perPage: perPageForResponse,
240
+ hasMore: perPageInput === false ? false : end < total
241
+ };
242
+ } catch (error) {
243
+ const mastraError = new MastraError({
244
+ id: createStorageErrorId("REDIS", "LIST_THREADS", "FAILED"),
245
+ domain: ErrorDomain.STORAGE,
246
+ category: ErrorCategory.THIRD_PARTY,
247
+ details: {
248
+ ...filter?.resourceId && { resourceId: filter.resourceId },
249
+ hasMetadataFilter: !!filter?.metadata,
250
+ page,
251
+ perPage
252
+ }
253
+ }, error);
254
+ this.logger.trackException(mastraError);
255
+ this.logger.error(mastraError.toString());
256
+ return {
257
+ threads: [],
258
+ total: 0,
259
+ page,
260
+ perPage: perPageForResponse,
261
+ hasMore: false
262
+ };
263
+ }
264
+ }
265
+ async saveThread({ thread }) {
266
+ try {
267
+ await this.db.insert({
268
+ tableName: TABLE_THREADS,
269
+ record: thread
270
+ });
271
+ return thread;
272
+ } catch (error) {
273
+ const mastraError = new MastraError({
274
+ id: createStorageErrorId("REDIS", "SAVE_THREAD", "FAILED"),
275
+ domain: ErrorDomain.STORAGE,
276
+ category: ErrorCategory.THIRD_PARTY,
277
+ details: { threadId: thread.id }
278
+ }, error);
279
+ this.logger.trackException(mastraError);
280
+ this.logger.error(mastraError.toString());
281
+ throw mastraError;
282
+ }
283
+ }
284
+ async updateThread({ id, title, metadata }) {
285
+ const thread = await this.getThreadById({ threadId: id });
286
+ if (!thread) throw new MastraError({
287
+ id: createStorageErrorId("REDIS", "UPDATE_THREAD", "FAILED"),
288
+ domain: ErrorDomain.STORAGE,
289
+ category: ErrorCategory.USER,
290
+ text: `Thread ${id} not found`,
291
+ details: { threadId: id }
292
+ });
293
+ const updatedThread = {
294
+ ...thread,
295
+ title,
296
+ metadata: {
297
+ ...thread.metadata,
298
+ ...metadata
299
+ },
300
+ updatedAt: /* @__PURE__ */ new Date()
301
+ };
302
+ try {
303
+ await this.saveThread({ thread: updatedThread });
304
+ return updatedThread;
305
+ } catch (error) {
306
+ throw new MastraError({
307
+ id: createStorageErrorId("REDIS", "UPDATE_THREAD", "FAILED"),
308
+ domain: ErrorDomain.STORAGE,
309
+ category: ErrorCategory.THIRD_PARTY,
310
+ details: { threadId: id }
311
+ }, error);
312
+ }
313
+ }
314
+ async deleteThread({ threadId }) {
315
+ const threadKey = getKey(TABLE_THREADS, { id: threadId });
316
+ const threadMessagesKey = getThreadMessagesKey(threadId);
317
+ try {
318
+ const messageIds = await this.client.zRange(threadMessagesKey, 0, -1);
319
+ const multi = this.client.multi();
320
+ multi.del(threadKey);
321
+ multi.del(threadMessagesKey);
322
+ for (const messageId of messageIds) {
323
+ const messageKey = getMessageKey(threadId, messageId);
324
+ multi.del(messageKey);
325
+ multi.del(getMessageIndexKey(messageId));
326
+ }
327
+ await multi.exec();
328
+ await this.db.scanAndDelete(getMessageKey(threadId, "*"));
329
+ } catch (error) {
330
+ throw new MastraError({
331
+ id: createStorageErrorId("REDIS", "DELETE_THREAD", "FAILED"),
332
+ domain: ErrorDomain.STORAGE,
333
+ category: ErrorCategory.THIRD_PARTY,
334
+ details: { threadId }
335
+ }, error);
336
+ }
337
+ }
338
+ async saveMessages(args) {
339
+ const { messages } = args;
340
+ if (messages.length === 0) return { messages: [] };
341
+ const threadId = messages[0]?.threadId;
342
+ try {
343
+ if (!threadId) throw new Error("Thread ID is required");
344
+ if (!await this.getThreadById({ threadId })) throw new Error(`Thread ${threadId} not found`);
345
+ } catch (error) {
346
+ throw new MastraError({
347
+ id: createStorageErrorId("REDIS", "SAVE_MESSAGES", "INVALID_ARGS"),
348
+ domain: ErrorDomain.STORAGE,
349
+ category: ErrorCategory.USER
350
+ }, error);
351
+ }
352
+ const messagesWithIndex = messages.map((message, index) => {
353
+ if (!message.threadId) throw new Error(`Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`);
354
+ if (!message.resourceId) throw new Error(`Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`);
355
+ return {
356
+ ...message,
357
+ _index: index
358
+ };
359
+ });
360
+ const threadKey = getKey(TABLE_THREADS, { id: threadId });
361
+ const existingThreadData = await this.client.get(threadKey);
362
+ const existingThread = existingThreadData ? JSON.parse(existingThreadData) : null;
363
+ try {
364
+ const batchSize = 1e3;
365
+ const existingThreadIds = await this.client.mGet(messagesWithIndex.map((message) => getMessageIndexKey(message.id)));
366
+ for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
367
+ const batch = messagesWithIndex.slice(i, i + batchSize);
368
+ const batchExistingThreadIds = existingThreadIds.slice(i, i + batch.length);
369
+ const multi = this.client.multi();
370
+ for (const [batchIndex, message] of batch.entries()) {
371
+ const key = getMessageKey(message.threadId, message.id);
372
+ const score = getMessageScore(message);
373
+ const existingThreadId = batchExistingThreadIds[batchIndex];
374
+ if (existingThreadId && existingThreadId !== message.threadId) {
375
+ const existingMessageKey = getMessageKey(existingThreadId, message.id);
376
+ multi.del(existingMessageKey);
377
+ multi.zRem(getThreadMessagesKey(existingThreadId), message.id);
378
+ }
379
+ multi.set(key, JSON.stringify(message));
380
+ multi.set(getMessageIndexKey(message.id), message.threadId);
381
+ multi.zAdd(getThreadMessagesKey(message.threadId), {
382
+ score,
383
+ value: message.id
384
+ });
385
+ }
386
+ if (i === 0 && existingThread) {
387
+ const updatedThread = {
388
+ ...existingThread,
389
+ updatedAt: /* @__PURE__ */ new Date()
390
+ };
391
+ multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, updatedThread).processedRecord));
392
+ }
393
+ await multi.exec();
394
+ }
395
+ return { messages: new MessageList().add(messages, "memory").get.all.db() };
396
+ } catch (error) {
397
+ throw new MastraError({
398
+ id: createStorageErrorId("REDIS", "SAVE_MESSAGES", "FAILED"),
399
+ domain: ErrorDomain.STORAGE,
400
+ category: ErrorCategory.THIRD_PARTY,
401
+ details: { threadId }
402
+ }, error);
403
+ }
404
+ }
405
+ async getThreadIdForMessage(messageId) {
406
+ const indexedThreadId = await this.client.get(getMessageIndexKey(messageId));
407
+ if (indexedThreadId) return indexedThreadId;
408
+ const keys = await this.db.scanKeys(getMessageKey("*", messageId));
409
+ if (keys.length === 0) return null;
410
+ const messageData = await this.client.get(keys[0]);
411
+ if (!messageData) return null;
412
+ const message = JSON.parse(messageData);
413
+ if (message.threadId) await this.client.set(getMessageIndexKey(messageId), message.threadId);
414
+ return message.threadId || null;
415
+ }
416
+ async getIncludedMessages(include) {
417
+ if (!include?.length) return [];
418
+ const messageIds = /* @__PURE__ */ new Set();
419
+ const messageIdToThreadIds = {};
420
+ for (const item of include) {
421
+ const itemThreadId = await this.getThreadIdForMessage(item.id);
422
+ if (!itemThreadId) continue;
423
+ messageIds.add(item.id);
424
+ messageIdToThreadIds[item.id] = itemThreadId;
425
+ const itemThreadMessagesKey = getThreadMessagesKey(itemThreadId);
426
+ const rank = await this.client.zRank(itemThreadMessagesKey, item.id);
427
+ if (rank === null) continue;
428
+ if (item.withPreviousMessages) {
429
+ const start = Math.max(0, rank - item.withPreviousMessages);
430
+ (rank === 0 ? [] : await this.client.zRange(itemThreadMessagesKey, start, rank - 1)).forEach((id) => {
431
+ messageIds.add(id);
432
+ messageIdToThreadIds[id] = itemThreadId;
433
+ });
434
+ }
435
+ if (item.withNextMessages) (await this.client.zRange(itemThreadMessagesKey, rank + 1, rank + item.withNextMessages)).forEach((id) => {
436
+ messageIds.add(id);
437
+ messageIdToThreadIds[id] = itemThreadId;
438
+ });
439
+ }
440
+ if (messageIds.size === 0) return [];
441
+ const keysToFetch = Array.from(messageIds).map((id) => getMessageKey(messageIdToThreadIds[id], id));
442
+ return (await this.client.mGet(keysToFetch)).filter((data) => data !== null).map((data) => JSON.parse(data));
443
+ }
444
+ parseStoredMessage(storedMessage) {
445
+ const defaultMessageContent = {
446
+ format: 2,
447
+ parts: [{
448
+ type: "text",
449
+ text: ""
450
+ }]
451
+ };
452
+ const { _index, ...rest } = storedMessage;
453
+ return {
454
+ ...rest,
455
+ createdAt: new Date(rest.createdAt),
456
+ content: rest.content || defaultMessageContent
457
+ };
458
+ }
459
+ async listMessagesById({ messageIds }) {
460
+ if (messageIds.length === 0) return { messages: [] };
461
+ try {
462
+ const rawMessages = [];
463
+ const indexKeys = messageIds.map((id) => getMessageIndexKey(id));
464
+ const indexResults = await this.client.mGet(indexKeys);
465
+ const indexedIds = [];
466
+ const unindexedIds = [];
467
+ messageIds.forEach((id, i) => {
468
+ const threadId = indexResults[i];
469
+ if (threadId) {
470
+ indexedIds.push({
471
+ messageId: id,
472
+ threadId
473
+ });
474
+ return;
475
+ }
476
+ unindexedIds.push(id);
477
+ });
478
+ if (indexedIds.length > 0) {
479
+ const messageKeys = indexedIds.map(({ messageId, threadId }) => getMessageKey(threadId, messageId));
480
+ const messageResults = await this.client.mGet(messageKeys);
481
+ for (const data of messageResults) if (data) rawMessages.push(JSON.parse(data));
482
+ }
483
+ if (unindexedIds.length > 0) {
484
+ const threadKeys = await this.db.scanKeys("thread:*:messages");
485
+ const foundMessages = (await Promise.all(threadKeys.map(async (threadKey) => {
486
+ const threadId = threadKey.split(":")[1];
487
+ if (!threadId) throw new Error(`Failed to parse thread ID from thread key "${threadKey}"`);
488
+ const msgKeys = unindexedIds.map((id) => getMessageKey(threadId, id));
489
+ return this.client.mGet(msgKeys);
490
+ }))).flat(1).filter((data) => !!data).map((data) => JSON.parse(data));
491
+ rawMessages.push(...foundMessages);
492
+ if (foundMessages.length > 0) {
493
+ const multi = this.client.multi();
494
+ foundMessages.forEach((msg) => {
495
+ if (msg.threadId) multi.set(getMessageIndexKey(msg.id), msg.threadId);
496
+ });
497
+ await multi.exec();
498
+ }
499
+ }
500
+ return { messages: new MessageList().add(rawMessages.map(this.parseStoredMessage), "memory").get.all.db() };
501
+ } catch (error) {
502
+ throw new MastraError({
503
+ id: createStorageErrorId("REDIS", "LIST_MESSAGES_BY_ID", "FAILED"),
504
+ domain: ErrorDomain.STORAGE,
505
+ category: ErrorCategory.THIRD_PARTY,
506
+ details: { messageIds: JSON.stringify(messageIds) }
507
+ }, error);
508
+ }
509
+ }
510
+ async listMessages(args) {
511
+ const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
512
+ const threadIds = Array.isArray(threadId) ? threadId : [threadId];
513
+ const threadIdsSet = new Set(threadIds);
514
+ if (threadIds.length === 0 || threadIds.some((id) => !id.trim())) throw new MastraError({
515
+ id: createStorageErrorId("REDIS", "LIST_MESSAGES", "INVALID_THREAD_ID"),
516
+ domain: ErrorDomain.STORAGE,
517
+ category: ErrorCategory.USER,
518
+ details: { threadId: Array.isArray(threadId) ? threadId.join(",") : threadId }
519
+ }, /* @__PURE__ */ new Error("threadId must be a non-empty string or array of non-empty strings"));
520
+ const perPage = normalizePerPage(perPageInput, 40);
521
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
522
+ const metadataFilter = validateStorageMetadataFilter(filter?.metadata);
523
+ try {
524
+ if (page < 0) throw new MastraError({
525
+ id: createStorageErrorId("REDIS", "LIST_MESSAGES", "INVALID_PAGE"),
526
+ domain: ErrorDomain.STORAGE,
527
+ category: ErrorCategory.USER,
528
+ details: { page }
529
+ }, /* @__PURE__ */ new Error("page must be >= 0"));
530
+ const { field, direction } = this.parseOrderBy(orderBy, "ASC");
531
+ const getFieldValue = (msg) => {
532
+ if (field === "createdAt") return new Date(msg.createdAt).getTime();
533
+ const value = msg[field];
534
+ if (typeof value === "number") return value;
535
+ if (value instanceof Date) return value.getTime();
536
+ return 0;
537
+ };
538
+ if (perPage === 0 && (!include || include.length === 0)) return {
539
+ messages: [],
540
+ total: 0,
541
+ page,
542
+ perPage: perPageForResponse,
543
+ hasMore: false
544
+ };
545
+ let includedMessages = [];
546
+ if (include && include.length > 0) includedMessages = (await this.getIncludedMessages(include)).map(this.parseStoredMessage);
547
+ if (perPage === 0 && include && include.length > 0) return {
548
+ messages: new MessageList().add(includedMessages, "memory").get.all.db().sort((a, b) => {
549
+ const aValue = getFieldValue(a);
550
+ const bValue = getFieldValue(b);
551
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
552
+ }),
553
+ total: 0,
554
+ page,
555
+ perPage: perPageForResponse,
556
+ hasMore: false
557
+ };
558
+ const allMessageIdsWithThreads = [];
559
+ for (const tid of threadIds) {
560
+ const threadMessagesKey = getThreadMessagesKey(tid);
561
+ const msgIds = await this.client.zRange(threadMessagesKey, 0, -1);
562
+ for (const mid of msgIds) allMessageIdsWithThreads.push({
563
+ threadId: tid,
564
+ messageId: mid
565
+ });
566
+ }
567
+ if (allMessageIdsWithThreads.length === 0) return {
568
+ messages: [],
569
+ total: 0,
570
+ page,
571
+ perPage: perPageForResponse,
572
+ hasMore: false
573
+ };
574
+ const messageKeys = allMessageIdsWithThreads.map(({ threadId: tid, messageId }) => getMessageKey(tid, messageId));
575
+ let messagesData = (await this.client.mGet(messageKeys)).filter((data) => data !== null).map((data) => JSON.parse(data)).map(this.parseStoredMessage);
576
+ if (resourceId) messagesData = messagesData.filter((msg) => msg.resourceId === resourceId);
577
+ messagesData = filterByDateRange(messagesData, (msg) => new Date(msg.createdAt), filter?.dateRange);
578
+ messagesData = messagesData.filter((message) => storageMessageMatchesMetadataFilter(message.content, metadataFilter));
579
+ messagesData.sort((a, b) => {
580
+ const aValue = getFieldValue(a);
581
+ const bValue = getFieldValue(b);
582
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
583
+ });
584
+ const total = messagesData.length;
585
+ const start = offset;
586
+ const end = perPageInput === false ? total : start + perPage;
587
+ const paginatedMessages = messagesData.slice(start, end);
588
+ const messageIdsSet = /* @__PURE__ */ new Set();
589
+ const allMessages = [];
590
+ for (const msg of paginatedMessages) {
591
+ if (messageIdsSet.has(msg.id)) continue;
592
+ allMessages.push(msg);
593
+ messageIdsSet.add(msg.id);
594
+ }
595
+ for (const msg of includedMessages) {
596
+ if (messageIdsSet.has(msg.id)) continue;
597
+ allMessages.push(msg);
598
+ messageIdsSet.add(msg.id);
599
+ }
600
+ let finalMessages = new MessageList().add(allMessages, "memory").get.all.db();
601
+ finalMessages = finalMessages.sort((a, b) => {
602
+ const aValue = getFieldValue(a);
603
+ const bValue = getFieldValue(b);
604
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
605
+ });
606
+ const returnedThreadMessageIds = new Set(finalMessages.filter((message) => message.threadId && threadIdsSet.has(message.threadId)).map((message) => message.id));
607
+ const hasMore = perPageInput !== false && (metadataFilter || returnedThreadMessageIds.size < total) && offset + paginatedMessages.length < total;
608
+ return {
609
+ messages: finalMessages,
610
+ total,
611
+ page,
612
+ perPage: perPageForResponse,
613
+ hasMore
614
+ };
615
+ } catch (error) {
616
+ const mastraError = new MastraError({
617
+ id: createStorageErrorId("REDIS", "LIST_MESSAGES", "FAILED"),
618
+ domain: ErrorDomain.STORAGE,
619
+ category: ErrorCategory.THIRD_PARTY,
620
+ details: {
621
+ threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
622
+ resourceId: resourceId ?? ""
623
+ }
624
+ }, error);
625
+ this.logger.error(mastraError.toString());
626
+ this.logger.trackException(mastraError);
627
+ return {
628
+ messages: [],
629
+ total: 0,
630
+ page,
631
+ perPage: perPageForResponse,
632
+ hasMore: false
633
+ };
634
+ }
635
+ }
636
+ async getResourceById({ resourceId }) {
637
+ try {
638
+ const key = `${TABLE_RESOURCES}:${resourceId}`;
639
+ const data = await this.client.get(key);
640
+ if (!data) return null;
641
+ const resource = JSON.parse(data);
642
+ return {
643
+ ...resource,
644
+ createdAt: new Date(resource.createdAt),
645
+ updatedAt: new Date(resource.updatedAt),
646
+ workingMemory: typeof resource.workingMemory === "object" ? JSON.stringify(resource.workingMemory) : resource.workingMemory,
647
+ metadata: typeof resource.metadata === "string" ? JSON.parse(resource.metadata) : resource.metadata
648
+ };
649
+ } catch (error) {
650
+ this.logger.error("Error getting resource by ID:", error);
651
+ throw error;
652
+ }
653
+ }
654
+ async saveResource({ resource }) {
655
+ try {
656
+ const key = `${TABLE_RESOURCES}:${resource.id}`;
657
+ const serializedResource = {
658
+ ...resource,
659
+ metadata: JSON.stringify(resource.metadata),
660
+ createdAt: resource.createdAt.toISOString(),
661
+ updatedAt: resource.updatedAt.toISOString()
662
+ };
663
+ await this.client.set(key, JSON.stringify(serializedResource));
664
+ return resource;
665
+ } catch (error) {
666
+ this.logger.error("Error saving resource:", error);
667
+ throw error;
668
+ }
669
+ }
670
+ async updateResource({ resourceId, workingMemory, metadata }) {
671
+ try {
672
+ const existingResource = await this.getResourceById({ resourceId });
673
+ if (!existingResource) {
674
+ const newResource = {
675
+ id: resourceId,
676
+ workingMemory,
677
+ metadata: metadata || {},
678
+ createdAt: /* @__PURE__ */ new Date(),
679
+ updatedAt: /* @__PURE__ */ new Date()
680
+ };
681
+ return this.saveResource({ resource: newResource });
682
+ }
683
+ const updatedResource = {
684
+ ...existingResource,
685
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
686
+ metadata: {
687
+ ...existingResource.metadata,
688
+ ...metadata
689
+ },
690
+ updatedAt: /* @__PURE__ */ new Date()
691
+ };
692
+ await this.saveResource({ resource: updatedResource });
693
+ return updatedResource;
694
+ } catch (error) {
695
+ this.logger.error("Error updating resource:", error);
696
+ throw error;
697
+ }
698
+ }
699
+ async updateMessages(args) {
700
+ const { messages } = args;
701
+ if (messages.length === 0) return [];
702
+ try {
703
+ const messageIds = messages.map((m) => m.id);
704
+ const existingMessages = [];
705
+ const messageIdToKey = {};
706
+ for (const messageId of messageIds) {
707
+ const pattern = getMessageKey("*", messageId);
708
+ const keys = await this.db.scanKeys(pattern);
709
+ for (const key of keys) {
710
+ const data = await this.client.get(key);
711
+ if (!data) continue;
712
+ const message = JSON.parse(data);
713
+ if (message && message.id === messageId) {
714
+ existingMessages.push(message);
715
+ messageIdToKey[messageId] = key;
716
+ break;
717
+ }
718
+ }
719
+ }
720
+ if (existingMessages.length === 0) return [];
721
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
722
+ const multi = this.client.multi();
723
+ for (const existingMessage of existingMessages) {
724
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
725
+ if (!updatePayload) continue;
726
+ const { id, ...fieldsToUpdate } = updatePayload;
727
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
728
+ threadIdsToUpdate.add(existingMessage.threadId);
729
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) threadIdsToUpdate.add(updatePayload.threadId);
730
+ const updatedMessage = { ...existingMessage };
731
+ if (fieldsToUpdate.content) {
732
+ const existingContent = existingMessage.content;
733
+ updatedMessage.content = {
734
+ ...existingContent,
735
+ ...fieldsToUpdate.content,
736
+ ...existingContent?.metadata && fieldsToUpdate.content.metadata ? { metadata: {
737
+ ...existingContent.metadata,
738
+ ...fieldsToUpdate.content.metadata
739
+ } } : {}
740
+ };
741
+ }
742
+ for (const key in fieldsToUpdate) if (Object.prototype.hasOwnProperty.call(fieldsToUpdate, key) && key !== "content") updatedMessage[key] = fieldsToUpdate[key];
743
+ const key = messageIdToKey[id];
744
+ if (!key) continue;
745
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
746
+ multi.zRem(getThreadMessagesKey(existingMessage.threadId), id);
747
+ multi.del(key);
748
+ const newKey = getMessageKey(updatePayload.threadId, id);
749
+ multi.set(newKey, JSON.stringify(updatedMessage));
750
+ multi.set(getMessageIndexKey(id), updatePayload.threadId);
751
+ const score = getMessageScore(updatedMessage);
752
+ multi.zAdd(getThreadMessagesKey(updatePayload.threadId), {
753
+ score,
754
+ value: id
755
+ });
756
+ messageIdToKey[id] = newKey;
757
+ continue;
758
+ }
759
+ multi.set(key, JSON.stringify(updatedMessage));
760
+ }
761
+ const now = /* @__PURE__ */ new Date();
762
+ for (const threadId of threadIdsToUpdate) if (threadId) {
763
+ const threadKey = getKey(TABLE_THREADS, { id: threadId });
764
+ const existingThreadData = await this.client.get(threadKey);
765
+ if (existingThreadData) {
766
+ const updatedThread = {
767
+ ...JSON.parse(existingThreadData),
768
+ updatedAt: now
769
+ };
770
+ multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, updatedThread).processedRecord));
771
+ }
772
+ }
773
+ await multi.exec();
774
+ const updatedMessages = [];
775
+ for (const messageId of messageIds) {
776
+ const key = messageIdToKey[messageId];
777
+ if (key) {
778
+ const data = await this.client.get(key);
779
+ if (data) updatedMessages.push(JSON.parse(data));
780
+ }
781
+ }
782
+ return updatedMessages;
783
+ } catch (error) {
784
+ throw new MastraError({
785
+ id: createStorageErrorId("REDIS", "UPDATE_MESSAGES", "FAILED"),
786
+ domain: ErrorDomain.STORAGE,
787
+ category: ErrorCategory.THIRD_PARTY,
788
+ details: { messageIds: messages.map((m) => m.id).join(",") }
789
+ }, error);
790
+ }
791
+ }
792
+ async deleteMessages(messageIds) {
793
+ if (!messageIds || messageIds.length === 0) return;
794
+ try {
795
+ const threadIds = /* @__PURE__ */ new Set();
796
+ const messageKeys = [];
797
+ const foundMessageIds = [];
798
+ const messageIdToThreadId = /* @__PURE__ */ new Map();
799
+ const indexKeys = messageIds.map((id) => getMessageIndexKey(id));
800
+ const indexResults = await this.client.mGet(indexKeys);
801
+ const indexedMessages = [];
802
+ const unindexedMessageIds = [];
803
+ messageIds.forEach((id, i) => {
804
+ const threadId = indexResults[i];
805
+ if (threadId) {
806
+ indexedMessages.push({
807
+ messageId: id,
808
+ threadId
809
+ });
810
+ return;
811
+ }
812
+ unindexedMessageIds.push(id);
813
+ });
814
+ for (const { messageId, threadId } of indexedMessages) {
815
+ messageKeys.push(getMessageKey(threadId, messageId));
816
+ foundMessageIds.push(messageId);
817
+ messageIdToThreadId.set(messageId, threadId);
818
+ threadIds.add(threadId);
819
+ }
820
+ for (const messageId of unindexedMessageIds) {
821
+ const pattern = getMessageKey("*", messageId);
822
+ const keys = await this.db.scanKeys(pattern);
823
+ for (const key of keys) {
824
+ const data = await this.client.get(key);
825
+ if (!data) continue;
826
+ const message = JSON.parse(data);
827
+ if (message && message.id === messageId) {
828
+ messageKeys.push(key);
829
+ foundMessageIds.push(messageId);
830
+ if (message.threadId) {
831
+ messageIdToThreadId.set(messageId, message.threadId);
832
+ threadIds.add(message.threadId);
833
+ }
834
+ break;
835
+ }
836
+ }
837
+ }
838
+ if (messageKeys.length === 0) return;
839
+ const multi = this.client.multi();
840
+ for (const key of messageKeys) multi.del(key);
841
+ for (const messageId of foundMessageIds) multi.del(getMessageIndexKey(messageId));
842
+ if (threadIds.size > 0) for (const threadId of threadIds) {
843
+ for (const [msgId, msgThreadId] of messageIdToThreadId) if (msgThreadId === threadId) multi.zRem(getThreadMessagesKey(threadId), msgId);
844
+ const threadKey = getKey(TABLE_THREADS, { id: threadId });
845
+ const threadData = await this.client.get(threadKey);
846
+ if (!threadData) continue;
847
+ const updatedThread = {
848
+ ...JSON.parse(threadData),
849
+ updatedAt: /* @__PURE__ */ new Date()
850
+ };
851
+ multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, updatedThread).processedRecord));
852
+ }
853
+ await multi.exec();
854
+ } catch (error) {
855
+ throw new MastraError({
856
+ id: createStorageErrorId("REDIS", "DELETE_MESSAGES", "FAILED"),
857
+ domain: ErrorDomain.STORAGE,
858
+ category: ErrorCategory.THIRD_PARTY,
859
+ details: { messageIds: messageIds.join(", ") }
860
+ }, error);
861
+ }
862
+ }
863
+ sortThreads(threads, field, direction) {
864
+ return threads.sort((a, b) => {
865
+ const aValue = new Date(a[field]).getTime();
866
+ const bValue = new Date(b[field]).getTime();
867
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
868
+ });
869
+ }
870
+ async cloneThread(args) {
871
+ const { sourceThreadId, newThreadId: providedThreadId, resourceId, title, metadata, options } = args;
872
+ const sourceThread = await this.getThreadById({ threadId: sourceThreadId });
873
+ if (!sourceThread) throw new MastraError({
874
+ id: createStorageErrorId("REDIS", "CLONE_THREAD", "SOURCE_NOT_FOUND"),
875
+ domain: ErrorDomain.STORAGE,
876
+ category: ErrorCategory.USER,
877
+ text: `Source thread with id ${sourceThreadId} not found`,
878
+ details: { sourceThreadId }
879
+ });
880
+ const newThreadId = providedThreadId || crypto.randomUUID();
881
+ if (await this.getThreadById({ threadId: newThreadId })) throw new MastraError({
882
+ id: createStorageErrorId("REDIS", "CLONE_THREAD", "THREAD_EXISTS"),
883
+ domain: ErrorDomain.STORAGE,
884
+ category: ErrorCategory.USER,
885
+ text: `Thread with id ${newThreadId} already exists`,
886
+ details: { newThreadId }
887
+ });
888
+ try {
889
+ const threadMessagesKey = getThreadMessagesKey(sourceThreadId);
890
+ const messageKeys = (await this.client.zRange(threadMessagesKey, 0, -1)).map((mid) => getMessageKey(sourceThreadId, mid));
891
+ let sourceMessages = [];
892
+ if (messageKeys.length > 0) sourceMessages = (await this.client.mGet(messageKeys)).filter((data) => data !== null).map((data) => {
893
+ const msg = JSON.parse(data);
894
+ return {
895
+ ...msg,
896
+ createdAt: new Date(msg.createdAt)
897
+ };
898
+ });
899
+ if (options?.messageFilter?.startDate || options?.messageFilter?.endDate) sourceMessages = filterByDateRange(sourceMessages, (msg) => new Date(msg.createdAt), {
900
+ start: options.messageFilter?.startDate,
901
+ end: options.messageFilter?.endDate
902
+ });
903
+ if (options?.messageFilter?.messageIds && options.messageFilter.messageIds.length > 0) {
904
+ const messageIdSet = new Set(options.messageFilter.messageIds);
905
+ sourceMessages = sourceMessages.filter((msg) => messageIdSet.has(msg.id));
906
+ }
907
+ sourceMessages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
908
+ if (options?.messageLimit && options.messageLimit > 0 && sourceMessages.length > options.messageLimit) sourceMessages = sourceMessages.slice(-options.messageLimit);
909
+ const now = /* @__PURE__ */ new Date();
910
+ const lastMessageId = sourceMessages.length > 0 ? sourceMessages[sourceMessages.length - 1].id : void 0;
911
+ const cloneMetadata = {
912
+ sourceThreadId,
913
+ clonedAt: now,
914
+ ...lastMessageId && { lastMessageId }
915
+ };
916
+ const newThread = {
917
+ id: newThreadId,
918
+ resourceId: resourceId || sourceThread.resourceId,
919
+ title: title || (sourceThread.title ? `Clone of ${sourceThread.title}` : void 0),
920
+ metadata: {
921
+ ...metadata,
922
+ clone: cloneMetadata
923
+ },
924
+ createdAt: now,
925
+ updatedAt: now
926
+ };
927
+ const multi = this.client.multi();
928
+ const threadKey = getKey(TABLE_THREADS, { id: newThreadId });
929
+ multi.set(threadKey, JSON.stringify(processRecord(TABLE_THREADS, newThread).processedRecord));
930
+ const clonedMessages = [];
931
+ const targetResourceId = resourceId || sourceThread.resourceId;
932
+ const newThreadMessagesKey = getThreadMessagesKey(newThreadId);
933
+ for (let i = 0; i < sourceMessages.length; i++) {
934
+ const sourceMsg = sourceMessages[i];
935
+ const newMessageId = crypto.randomUUID();
936
+ const { _index, ...restMsg } = sourceMsg;
937
+ const newMessage = {
938
+ ...restMsg,
939
+ id: newMessageId,
940
+ threadId: newThreadId,
941
+ resourceId: targetResourceId
942
+ };
943
+ const messageKey = getMessageKey(newThreadId, newMessageId);
944
+ multi.set(messageKey, JSON.stringify(newMessage));
945
+ multi.set(getMessageIndexKey(newMessageId), newThreadId);
946
+ const score = getMessageScore({
947
+ createdAt: newMessage.createdAt,
948
+ _index: i
949
+ });
950
+ multi.zAdd(newThreadMessagesKey, {
951
+ score,
952
+ value: newMessageId
953
+ });
954
+ clonedMessages.push(newMessage);
955
+ }
956
+ await multi.exec();
957
+ return {
958
+ thread: newThread,
959
+ clonedMessages
960
+ };
961
+ } catch (error) {
962
+ if (error instanceof MastraError) throw error;
963
+ throw new MastraError({
964
+ id: createStorageErrorId("REDIS", "CLONE_THREAD", "FAILED"),
965
+ domain: ErrorDomain.STORAGE,
966
+ category: ErrorCategory.THIRD_PARTY,
967
+ details: {
968
+ sourceThreadId,
969
+ newThreadId
970
+ }
971
+ }, error);
972
+ }
973
+ }
1175
974
  };
1176
975
  function getThreadMessagesKey(threadId) {
1177
- return `thread:${threadId}:messages`;
976
+ return `thread:${threadId}:messages`;
1178
977
  }
1179
978
  function getMessageKey(threadId, messageId) {
1180
- return getKey(TABLE_MESSAGES, { threadId, id: messageId });
979
+ return getKey(TABLE_MESSAGES, {
980
+ threadId,
981
+ id: messageId
982
+ });
1181
983
  }
1182
984
  function getMessageIndexKey(messageId) {
1183
- return `msg-idx:${messageId}`;
985
+ return `msg-idx:${messageId}`;
1184
986
  }
1185
987
  function getMessageScore(message) {
1186
- const createdAtScore = new Date(message.createdAt).getTime();
1187
- const index = typeof message._index === "number" ? message._index : 0;
1188
- return createdAtScore * 1e3 + index;
988
+ const createdAtScore = new Date(message.createdAt).getTime();
989
+ const index = typeof message._index === "number" ? message._index : 0;
990
+ return createdAtScore * 1e3 + index;
1189
991
  }
992
+ //#endregion
993
+ //#region src/storage/domains/scores/index.ts
994
+ /** Returns true when a row matches the multi-tenant scope filters (or none provided). */
1190
995
  function matchesTenancy(row, filters) {
1191
- if (filters?.organizationId !== void 0 && row.organizationId !== filters.organizationId) return false;
1192
- if (filters?.projectId !== void 0 && row.projectId !== filters.projectId) return false;
1193
- return true;
996
+ if (filters?.organizationId !== void 0 && row.organizationId !== filters.organizationId) return false;
997
+ if (filters?.projectId !== void 0 && row.projectId !== filters.projectId) return false;
998
+ return true;
1194
999
  }
1195
1000
  var ScoresRedis = class extends ScoresStorage {
1196
- client;
1197
- db;
1198
- constructor(config) {
1199
- super();
1200
- this.client = config.client;
1201
- this.db = new RedisDB({ client: config.client });
1202
- }
1203
- async dangerouslyClearAll() {
1204
- await this.db.deleteData({ tableName: TABLE_SCORERS });
1205
- }
1206
- async getScoreById({ id }) {
1207
- try {
1208
- const data = await this.db.get({
1209
- tableName: TABLE_SCORERS,
1210
- keys: { id }
1211
- });
1212
- if (!data) {
1213
- return null;
1214
- }
1215
- return transformScoreRow(data);
1216
- } catch (error) {
1217
- throw new MastraError(
1218
- {
1219
- id: createStorageErrorId("REDIS", "GET_SCORE_BY_ID", "FAILED"),
1220
- domain: ErrorDomain.STORAGE,
1221
- category: ErrorCategory.THIRD_PARTY,
1222
- details: {
1223
- ...id && { id }
1224
- }
1225
- },
1226
- error
1227
- );
1228
- }
1229
- }
1230
- async listScoresByScorerId({
1231
- scorerId,
1232
- entityId,
1233
- entityType,
1234
- source,
1235
- pagination = { page: 0, perPage: 20 },
1236
- filters
1237
- }) {
1238
- return this.fetchAndFilterScores(pagination, (row) => {
1239
- if (row.scorerId !== scorerId) {
1240
- return false;
1241
- }
1242
- if (entityId && row.entityId !== entityId) {
1243
- return false;
1244
- }
1245
- if (entityType && row.entityType !== entityType) {
1246
- return false;
1247
- }
1248
- if (source && row.source !== source) {
1249
- return false;
1250
- }
1251
- if (!matchesTenancy(row, filters)) {
1252
- return false;
1253
- }
1254
- return true;
1255
- });
1256
- }
1257
- async saveScore(score) {
1258
- let validatedScore;
1259
- try {
1260
- validatedScore = saveScorePayloadSchema.parse(score);
1261
- } catch (error) {
1262
- throw new MastraError(
1263
- {
1264
- id: createStorageErrorId("REDIS", "SAVE_SCORE", "VALIDATION_FAILED"),
1265
- domain: ErrorDomain.STORAGE,
1266
- category: ErrorCategory.USER,
1267
- details: {
1268
- scorer: typeof score.scorer?.id === "string" ? score.scorer.id : String(score.scorer?.id ?? "unknown"),
1269
- entityId: score.entityId ?? "unknown",
1270
- entityType: score.entityType ?? "unknown",
1271
- traceId: score.traceId ?? "",
1272
- spanId: score.spanId ?? ""
1273
- }
1274
- },
1275
- error
1276
- );
1277
- }
1278
- const now = /* @__PURE__ */ new Date();
1279
- const id = crypto2.randomUUID();
1280
- const scoreWithId = {
1281
- ...validatedScore,
1282
- id,
1283
- createdAt: now,
1284
- updatedAt: now
1285
- };
1286
- const { key, processedRecord } = processRecord(TABLE_SCORERS, scoreWithId);
1287
- try {
1288
- await this.client.set(key, JSON.stringify(processedRecord));
1289
- return { score: { ...validatedScore, id, createdAt: now, updatedAt: now } };
1290
- } catch (error) {
1291
- throw new MastraError(
1292
- {
1293
- id: createStorageErrorId("REDIS", "SAVE_SCORE", "FAILED"),
1294
- domain: ErrorDomain.STORAGE,
1295
- category: ErrorCategory.THIRD_PARTY,
1296
- details: { id }
1297
- },
1298
- error
1299
- );
1300
- }
1301
- }
1302
- async listScoresByRunId({
1303
- runId,
1304
- pagination = { page: 0, perPage: 20 },
1305
- filters
1306
- }) {
1307
- return this.fetchAndFilterScores(pagination, (row) => row.runId === runId && matchesTenancy(row, filters));
1308
- }
1309
- async listScoresByEntityId({
1310
- entityId,
1311
- entityType,
1312
- pagination = { page: 0, perPage: 20 },
1313
- filters
1314
- }) {
1315
- return this.fetchAndFilterScores(pagination, (row) => {
1316
- if (row.entityId !== entityId) {
1317
- return false;
1318
- }
1319
- if (entityType && row.entityType !== entityType) {
1320
- return false;
1321
- }
1322
- if (!matchesTenancy(row, filters)) {
1323
- return false;
1324
- }
1325
- return true;
1326
- });
1327
- }
1328
- async listScoresBySpan({
1329
- traceId,
1330
- spanId,
1331
- pagination = { page: 0, perPage: 20 },
1332
- filters
1333
- }) {
1334
- return this.fetchAndFilterScores(
1335
- pagination,
1336
- (row) => row.traceId === traceId && row.spanId === spanId && matchesTenancy(row, filters)
1337
- );
1338
- }
1339
- async fetchAndFilterScores(pagination, filterFn) {
1340
- const { page, perPage: perPageInput } = pagination;
1341
- const keys = await this.db.scanKeys(`${TABLE_SCORERS}:*`);
1342
- if (keys.length === 0) {
1343
- return {
1344
- scores: [],
1345
- pagination: { total: 0, page, perPage: perPageInput, hasMore: false }
1346
- };
1347
- }
1348
- const results = await this.client.mGet(keys);
1349
- const filtered = results.map((data) => {
1350
- if (!data) {
1351
- return null;
1352
- }
1353
- try {
1354
- return JSON.parse(data);
1355
- } catch {
1356
- return null;
1357
- }
1358
- }).filter((row) => !!row && typeof row === "object" && filterFn(row));
1359
- const total = filtered.length;
1360
- const perPage = normalizePerPage(perPageInput, 100);
1361
- const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1362
- const end = perPageInput === false ? total : start + perPage;
1363
- const scores = filtered.slice(start, end).map((row) => transformScoreRow(row));
1364
- return {
1365
- scores,
1366
- pagination: {
1367
- total,
1368
- page,
1369
- perPage: perPageForResponse,
1370
- hasMore: end < total
1371
- }
1372
- };
1373
- }
1001
+ client;
1002
+ db;
1003
+ constructor(config) {
1004
+ super();
1005
+ this.client = config.client;
1006
+ this.db = new RedisDB({ client: config.client });
1007
+ }
1008
+ async dangerouslyClearAll() {
1009
+ await this.db.deleteData({ tableName: TABLE_SCORERS });
1010
+ }
1011
+ async getScoreById({ id }) {
1012
+ try {
1013
+ const data = await this.db.get({
1014
+ tableName: TABLE_SCORERS,
1015
+ keys: { id }
1016
+ });
1017
+ if (!data) return null;
1018
+ return transformScoreRow(data);
1019
+ } catch (error) {
1020
+ throw new MastraError({
1021
+ id: createStorageErrorId("REDIS", "GET_SCORE_BY_ID", "FAILED"),
1022
+ domain: ErrorDomain.STORAGE,
1023
+ category: ErrorCategory.THIRD_PARTY,
1024
+ details: { ...id && { id } }
1025
+ }, error);
1026
+ }
1027
+ }
1028
+ async listScoresByScorerId({ scorerId, entityId, entityType, source, pagination = {
1029
+ page: 0,
1030
+ perPage: 20
1031
+ }, filters }) {
1032
+ return this.fetchAndFilterScores(pagination, (row) => {
1033
+ if (row.scorerId !== scorerId) return false;
1034
+ if (entityId && row.entityId !== entityId) return false;
1035
+ if (entityType && row.entityType !== entityType) return false;
1036
+ if (source && row.source !== source) return false;
1037
+ if (!matchesTenancy(row, filters)) return false;
1038
+ return true;
1039
+ });
1040
+ }
1041
+ async saveScore(score) {
1042
+ let validatedScore;
1043
+ try {
1044
+ validatedScore = saveScorePayloadSchema.parse(score);
1045
+ } catch (error) {
1046
+ throw new MastraError({
1047
+ id: createStorageErrorId("REDIS", "SAVE_SCORE", "VALIDATION_FAILED"),
1048
+ domain: ErrorDomain.STORAGE,
1049
+ category: ErrorCategory.USER,
1050
+ details: {
1051
+ scorer: typeof score.scorer?.id === "string" ? score.scorer.id : String(score.scorer?.id ?? "unknown"),
1052
+ entityId: score.entityId ?? "unknown",
1053
+ entityType: score.entityType ?? "unknown",
1054
+ traceId: score.traceId ?? "",
1055
+ spanId: score.spanId ?? ""
1056
+ }
1057
+ }, error);
1058
+ }
1059
+ const now = /* @__PURE__ */ new Date();
1060
+ const id = crypto$1.randomUUID();
1061
+ const { key, processedRecord } = processRecord(TABLE_SCORERS, {
1062
+ ...validatedScore,
1063
+ id,
1064
+ createdAt: now,
1065
+ updatedAt: now
1066
+ });
1067
+ try {
1068
+ await this.client.set(key, JSON.stringify(processedRecord));
1069
+ return { score: {
1070
+ ...validatedScore,
1071
+ id,
1072
+ createdAt: now,
1073
+ updatedAt: now
1074
+ } };
1075
+ } catch (error) {
1076
+ throw new MastraError({
1077
+ id: createStorageErrorId("REDIS", "SAVE_SCORE", "FAILED"),
1078
+ domain: ErrorDomain.STORAGE,
1079
+ category: ErrorCategory.THIRD_PARTY,
1080
+ details: { id }
1081
+ }, error);
1082
+ }
1083
+ }
1084
+ async listScoresByRunId({ runId, pagination = {
1085
+ page: 0,
1086
+ perPage: 20
1087
+ }, filters }) {
1088
+ return this.fetchAndFilterScores(pagination, (row) => row.runId === runId && matchesTenancy(row, filters));
1089
+ }
1090
+ async listScoresByEntityId({ entityId, entityType, pagination = {
1091
+ page: 0,
1092
+ perPage: 20
1093
+ }, filters }) {
1094
+ return this.fetchAndFilterScores(pagination, (row) => {
1095
+ if (row.entityId !== entityId) return false;
1096
+ if (entityType && row.entityType !== entityType) return false;
1097
+ if (!matchesTenancy(row, filters)) return false;
1098
+ return true;
1099
+ });
1100
+ }
1101
+ async listScoresBySpan({ traceId, spanId, pagination = {
1102
+ page: 0,
1103
+ perPage: 20
1104
+ }, filters }) {
1105
+ return this.fetchAndFilterScores(pagination, (row) => row.traceId === traceId && row.spanId === spanId && matchesTenancy(row, filters));
1106
+ }
1107
+ async fetchAndFilterScores(pagination, filterFn) {
1108
+ const { page, perPage: perPageInput } = pagination;
1109
+ const keys = await this.db.scanKeys(`${TABLE_SCORERS}:*`);
1110
+ if (keys.length === 0) return {
1111
+ scores: [],
1112
+ pagination: {
1113
+ total: 0,
1114
+ page,
1115
+ perPage: perPageInput,
1116
+ hasMore: false
1117
+ }
1118
+ };
1119
+ const filtered = (await this.client.mGet(keys)).map((data) => {
1120
+ if (!data) return null;
1121
+ try {
1122
+ return JSON.parse(data);
1123
+ } catch {
1124
+ return null;
1125
+ }
1126
+ }).filter((row) => !!row && typeof row === "object" && filterFn(row));
1127
+ const total = filtered.length;
1128
+ const perPage = normalizePerPage(perPageInput, 100);
1129
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1130
+ const end = perPageInput === false ? total : start + perPage;
1131
+ return {
1132
+ scores: filtered.slice(start, end).map((row) => transformScoreRow(row)),
1133
+ pagination: {
1134
+ total,
1135
+ page,
1136
+ perPage: perPageForResponse,
1137
+ hasMore: end < total
1138
+ }
1139
+ };
1140
+ }
1374
1141
  };
1142
+ //#endregion
1143
+ //#region src/storage/domains/workflows/index.ts
1375
1144
  function parseWorkflowRun(row) {
1376
- let parsedSnapshot = row.snapshot;
1377
- if (typeof parsedSnapshot === "string") {
1378
- try {
1379
- parsedSnapshot = JSON.parse(row.snapshot);
1380
- } catch (e) {
1381
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
1382
- }
1383
- }
1384
- return {
1385
- workflowName: row.workflow_name,
1386
- runId: row.run_id,
1387
- snapshot: parsedSnapshot,
1388
- createdAt: ensureDate(row.createdAt),
1389
- updatedAt: ensureDate(row.updatedAt),
1390
- resourceId: row.resourceId
1391
- };
1145
+ let parsedSnapshot = row.snapshot;
1146
+ if (typeof parsedSnapshot === "string") try {
1147
+ parsedSnapshot = JSON.parse(row.snapshot);
1148
+ } catch (e) {
1149
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
1150
+ }
1151
+ return {
1152
+ workflowName: row.workflow_name,
1153
+ runId: row.run_id,
1154
+ snapshot: parsedSnapshot,
1155
+ createdAt: ensureDate(row.createdAt),
1156
+ updatedAt: ensureDate(row.updatedAt),
1157
+ resourceId: row.resourceId
1158
+ };
1392
1159
  }
1393
1160
  var WorkflowsRedis = class extends WorkflowsStorage {
1394
- client;
1395
- db;
1396
- constructor(config) {
1397
- super();
1398
- this.client = config.client;
1399
- this.db = new RedisDB({ client: config.client });
1400
- }
1401
- supportsConcurrentUpdates() {
1402
- return false;
1403
- }
1404
- async dangerouslyClearAll() {
1405
- await this.db.deleteData({ tableName: TABLE_WORKFLOW_SNAPSHOT });
1406
- }
1407
- async updateWorkflowResults({
1408
- workflowName,
1409
- runId,
1410
- stepId,
1411
- result,
1412
- requestContext
1413
- }) {
1414
- try {
1415
- const existingRecord = await this.db.get({
1416
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1417
- keys: {
1418
- namespace: "workflows",
1419
- workflow_name: workflowName,
1420
- run_id: runId
1421
- }
1422
- });
1423
- const existingSnapshot = existingRecord?.snapshot;
1424
- let snapshot = existingSnapshot;
1425
- if (!snapshot) {
1426
- snapshot = {
1427
- context: {},
1428
- activePaths: [],
1429
- timestamp: Date.now(),
1430
- suspendedPaths: {},
1431
- activeStepsPath: {},
1432
- resumeLabels: {},
1433
- serializedStepGraph: [],
1434
- status: "pending",
1435
- value: {},
1436
- waitingPaths: {},
1437
- runId,
1438
- requestContext: {}
1439
- };
1440
- }
1441
- snapshot.context[stepId] = result;
1442
- snapshot.requestContext = { ...snapshot.requestContext, ...requestContext };
1443
- await this.persistWorkflowSnapshot({
1444
- namespace: "workflows",
1445
- workflowName,
1446
- runId,
1447
- snapshot,
1448
- createdAt: existingRecord?.createdAt ? ensureDate(existingRecord.createdAt) : void 0
1449
- });
1450
- return snapshot.context;
1451
- } catch (error) {
1452
- if (error instanceof MastraError) {
1453
- throw error;
1454
- }
1455
- throw new MastraError(
1456
- {
1457
- id: createStorageErrorId("REDIS", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
1458
- domain: ErrorDomain.STORAGE,
1459
- category: ErrorCategory.THIRD_PARTY,
1460
- details: { workflowName, runId, stepId }
1461
- },
1462
- error
1463
- );
1464
- }
1465
- }
1466
- async updateWorkflowState({
1467
- workflowName,
1468
- runId,
1469
- opts
1470
- }) {
1471
- try {
1472
- const existingRecord = await this.db.get({
1473
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1474
- keys: {
1475
- namespace: "workflows",
1476
- workflow_name: workflowName,
1477
- run_id: runId
1478
- }
1479
- });
1480
- const existingSnapshot = existingRecord?.snapshot;
1481
- if (!existingSnapshot || !existingSnapshot.context) {
1482
- return void 0;
1483
- }
1484
- const updatedSnapshot = { ...existingSnapshot, ...opts };
1485
- await this.persistWorkflowSnapshot({
1486
- namespace: "workflows",
1487
- workflowName,
1488
- runId,
1489
- snapshot: updatedSnapshot,
1490
- createdAt: existingRecord?.createdAt ? ensureDate(existingRecord.createdAt) : void 0
1491
- });
1492
- return updatedSnapshot;
1493
- } catch (error) {
1494
- if (error instanceof MastraError) {
1495
- throw error;
1496
- }
1497
- throw new MastraError(
1498
- {
1499
- id: createStorageErrorId("REDIS", "UPDATE_WORKFLOW_STATE", "FAILED"),
1500
- domain: ErrorDomain.STORAGE,
1501
- category: ErrorCategory.THIRD_PARTY,
1502
- details: { workflowName, runId }
1503
- },
1504
- error
1505
- );
1506
- }
1507
- }
1508
- async persistWorkflowSnapshot(params) {
1509
- const { namespace = "workflows", workflowName, runId, resourceId, snapshot, createdAt, updatedAt } = params;
1510
- try {
1511
- let finalCreatedAt = createdAt;
1512
- if (!finalCreatedAt) {
1513
- const existing = await this.db.get({
1514
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1515
- keys: {
1516
- namespace,
1517
- workflow_name: workflowName,
1518
- run_id: runId
1519
- }
1520
- });
1521
- finalCreatedAt = existing?.createdAt ? ensureDate(existing.createdAt) : /* @__PURE__ */ new Date();
1522
- }
1523
- await this.db.insert({
1524
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1525
- record: {
1526
- namespace,
1527
- workflow_name: workflowName,
1528
- run_id: runId,
1529
- resourceId,
1530
- snapshot,
1531
- createdAt: finalCreatedAt,
1532
- updatedAt: updatedAt ?? /* @__PURE__ */ new Date()
1533
- }
1534
- });
1535
- } catch (error) {
1536
- throw new MastraError(
1537
- {
1538
- id: createStorageErrorId("REDIS", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
1539
- domain: ErrorDomain.STORAGE,
1540
- category: ErrorCategory.THIRD_PARTY,
1541
- details: {
1542
- namespace,
1543
- workflowName,
1544
- runId
1545
- }
1546
- },
1547
- error
1548
- );
1549
- }
1550
- }
1551
- async loadWorkflowSnapshot(params) {
1552
- const { namespace = "workflows", workflowName, runId } = params;
1553
- const key = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1554
- namespace,
1555
- workflow_name: workflowName,
1556
- run_id: runId
1557
- });
1558
- try {
1559
- const data = await this.client.get(key);
1560
- if (!data) {
1561
- return null;
1562
- }
1563
- const parsed = JSON.parse(data);
1564
- return parsed.snapshot;
1565
- } catch (error) {
1566
- throw new MastraError(
1567
- {
1568
- id: createStorageErrorId("REDIS", "LOAD_WORKFLOW_SNAPSHOT", "FAILED"),
1569
- domain: ErrorDomain.STORAGE,
1570
- category: ErrorCategory.THIRD_PARTY,
1571
- details: {
1572
- namespace,
1573
- workflowName,
1574
- runId
1575
- }
1576
- },
1577
- error
1578
- );
1579
- }
1580
- }
1581
- async getWorkflowRunById({
1582
- runId,
1583
- workflowName
1584
- }) {
1585
- try {
1586
- const key = getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace: "workflows", workflow_name: workflowName, run_id: runId }) + "*";
1587
- const keys = await this.db.scanKeys(key);
1588
- if (keys.length === 0) {
1589
- return null;
1590
- }
1591
- const results = await this.client.mGet(keys);
1592
- const workflows = results.filter((data2) => data2 !== null).map(
1593
- (data2) => JSON.parse(data2)
1594
- );
1595
- const data = workflows.find((workflow) => {
1596
- if (!workflow) {
1597
- return false;
1598
- }
1599
- const runIdMatch = workflow.run_id === runId;
1600
- if (workflowName) {
1601
- return runIdMatch && workflow.workflow_name === workflowName;
1602
- }
1603
- return runIdMatch;
1604
- });
1605
- if (!data) {
1606
- return null;
1607
- }
1608
- return parseWorkflowRun(data);
1609
- } catch (error) {
1610
- throw new MastraError(
1611
- {
1612
- id: createStorageErrorId("REDIS", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
1613
- domain: ErrorDomain.STORAGE,
1614
- category: ErrorCategory.THIRD_PARTY,
1615
- details: {
1616
- namespace: "workflows",
1617
- runId,
1618
- workflowName: workflowName || ""
1619
- }
1620
- },
1621
- error
1622
- );
1623
- }
1624
- }
1625
- async deleteWorkflowRunById({ runId, workflowName }) {
1626
- const key = getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace: "workflows", workflow_name: workflowName, run_id: runId });
1627
- try {
1628
- await this.client.del(key);
1629
- } catch (error) {
1630
- throw new MastraError(
1631
- {
1632
- id: createStorageErrorId("REDIS", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
1633
- domain: ErrorDomain.STORAGE,
1634
- category: ErrorCategory.THIRD_PARTY,
1635
- details: {
1636
- namespace: "workflows",
1637
- runId,
1638
- workflowName
1639
- }
1640
- },
1641
- error
1642
- );
1643
- }
1644
- }
1645
- async listWorkflowRuns({
1646
- workflowName,
1647
- fromDate,
1648
- toDate,
1649
- perPage,
1650
- page,
1651
- resourceId,
1652
- status
1653
- } = {}) {
1654
- try {
1655
- if (page !== void 0 && page < 0) {
1656
- throw new MastraError(
1657
- {
1658
- id: createStorageErrorId("REDIS", "LIST_WORKFLOW_RUNS", "INVALID_PAGE"),
1659
- domain: ErrorDomain.STORAGE,
1660
- category: ErrorCategory.USER,
1661
- details: { page }
1662
- },
1663
- new Error("page must be >= 0")
1664
- );
1665
- }
1666
- const normalizedFrom = fromDate ? ensureDate(fromDate) : void 0;
1667
- const normalizedTo = toDate ? ensureDate(toDate) : void 0;
1668
- let pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace: "workflows" }) + ":*";
1669
- if (workflowName && resourceId) {
1670
- pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1671
- namespace: "workflows",
1672
- workflow_name: workflowName,
1673
- run_id: "*",
1674
- resourceId
1675
- });
1676
- } else if (workflowName) {
1677
- pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace: "workflows", workflow_name: workflowName }) + ":*";
1678
- } else if (resourceId) {
1679
- pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1680
- namespace: "workflows",
1681
- workflow_name: "*",
1682
- run_id: "*",
1683
- resourceId
1684
- });
1685
- }
1686
- const keys = await this.db.scanKeys(pattern);
1687
- if (keys.length === 0) {
1688
- return { runs: [], total: 0 };
1689
- }
1690
- const results = await this.client.mGet(keys);
1691
- let runs = results.filter((data) => data !== null).map((data) => JSON.parse(data)).filter(
1692
- (record) => record !== null && record !== void 0 && typeof record === "object" && "workflow_name" in record
1693
- ).filter((record) => !workflowName || record.workflow_name === workflowName).map((w) => parseWorkflowRun(w)).filter((w) => {
1694
- if (normalizedFrom && w.createdAt < normalizedFrom) {
1695
- return false;
1696
- }
1697
- if (normalizedTo && w.createdAt > normalizedTo) {
1698
- return false;
1699
- }
1700
- if (status) {
1701
- let snapshot = w.snapshot;
1702
- if (typeof snapshot === "string") {
1703
- try {
1704
- snapshot = JSON.parse(snapshot);
1705
- } catch (e) {
1706
- console.warn(`Failed to parse snapshot for workflow ${w.workflowName}: ${e}`);
1707
- return false;
1708
- }
1709
- }
1710
- return snapshot.status === status;
1711
- }
1712
- return true;
1713
- }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1714
- const total = runs.length;
1715
- if (typeof perPage === "number" && typeof page === "number") {
1716
- const normalizedPerPage = normalizePerPage(perPage, Number.MAX_SAFE_INTEGER);
1717
- const offset = page * normalizedPerPage;
1718
- runs = runs.slice(offset, offset + normalizedPerPage);
1719
- }
1720
- return { runs, total };
1721
- } catch (error) {
1722
- if (error instanceof MastraError) {
1723
- throw error;
1724
- }
1725
- throw new MastraError(
1726
- {
1727
- id: createStorageErrorId("REDIS", "LIST_WORKFLOW_RUNS", "FAILED"),
1728
- domain: ErrorDomain.STORAGE,
1729
- category: ErrorCategory.THIRD_PARTY,
1730
- details: {
1731
- namespace: "workflows",
1732
- workflowName: workflowName || "",
1733
- resourceId: resourceId || ""
1734
- }
1735
- },
1736
- error
1737
- );
1738
- }
1739
- }
1161
+ client;
1162
+ db;
1163
+ constructor(config) {
1164
+ super();
1165
+ this.client = config.client;
1166
+ this.db = new RedisDB({ client: config.client });
1167
+ }
1168
+ supportsConcurrentUpdates() {
1169
+ return false;
1170
+ }
1171
+ async dangerouslyClearAll() {
1172
+ await this.db.deleteData({ tableName: TABLE_WORKFLOW_SNAPSHOT });
1173
+ }
1174
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext }) {
1175
+ try {
1176
+ const existingRecord = await this.db.get({
1177
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
1178
+ keys: {
1179
+ namespace: "workflows",
1180
+ workflow_name: workflowName,
1181
+ run_id: runId
1182
+ }
1183
+ });
1184
+ let snapshot = existingRecord?.snapshot;
1185
+ if (!snapshot) snapshot = {
1186
+ context: {},
1187
+ activePaths: [],
1188
+ timestamp: Date.now(),
1189
+ suspendedPaths: {},
1190
+ activeStepsPath: {},
1191
+ resumeLabels: {},
1192
+ serializedStepGraph: [],
1193
+ status: "pending",
1194
+ value: {},
1195
+ waitingPaths: {},
1196
+ runId,
1197
+ requestContext: {}
1198
+ };
1199
+ snapshot.context[stepId] = result;
1200
+ snapshot.requestContext = {
1201
+ ...snapshot.requestContext,
1202
+ ...requestContext
1203
+ };
1204
+ await this.persistWorkflowSnapshot({
1205
+ namespace: "workflows",
1206
+ workflowName,
1207
+ runId,
1208
+ snapshot,
1209
+ createdAt: existingRecord?.createdAt ? ensureDate(existingRecord.createdAt) : void 0
1210
+ });
1211
+ return snapshot.context;
1212
+ } catch (error) {
1213
+ if (error instanceof MastraError) throw error;
1214
+ throw new MastraError({
1215
+ id: createStorageErrorId("REDIS", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
1216
+ domain: ErrorDomain.STORAGE,
1217
+ category: ErrorCategory.THIRD_PARTY,
1218
+ details: {
1219
+ workflowName,
1220
+ runId,
1221
+ stepId
1222
+ }
1223
+ }, error);
1224
+ }
1225
+ }
1226
+ async updateWorkflowState({ workflowName, runId, opts }) {
1227
+ try {
1228
+ const existingRecord = await this.db.get({
1229
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
1230
+ keys: {
1231
+ namespace: "workflows",
1232
+ workflow_name: workflowName,
1233
+ run_id: runId
1234
+ }
1235
+ });
1236
+ const existingSnapshot = existingRecord?.snapshot;
1237
+ if (!existingSnapshot || !existingSnapshot.context) return;
1238
+ const updatedSnapshot = {
1239
+ ...existingSnapshot,
1240
+ ...opts
1241
+ };
1242
+ await this.persistWorkflowSnapshot({
1243
+ namespace: "workflows",
1244
+ workflowName,
1245
+ runId,
1246
+ snapshot: updatedSnapshot,
1247
+ createdAt: existingRecord?.createdAt ? ensureDate(existingRecord.createdAt) : void 0
1248
+ });
1249
+ return updatedSnapshot;
1250
+ } catch (error) {
1251
+ if (error instanceof MastraError) throw error;
1252
+ throw new MastraError({
1253
+ id: createStorageErrorId("REDIS", "UPDATE_WORKFLOW_STATE", "FAILED"),
1254
+ domain: ErrorDomain.STORAGE,
1255
+ category: ErrorCategory.THIRD_PARTY,
1256
+ details: {
1257
+ workflowName,
1258
+ runId
1259
+ }
1260
+ }, error);
1261
+ }
1262
+ }
1263
+ async persistWorkflowSnapshot(params) {
1264
+ const { namespace = "workflows", workflowName, runId, resourceId, snapshot, createdAt, updatedAt } = params;
1265
+ try {
1266
+ let finalCreatedAt = createdAt;
1267
+ if (!finalCreatedAt) {
1268
+ const existing = await this.db.get({
1269
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
1270
+ keys: {
1271
+ namespace,
1272
+ workflow_name: workflowName,
1273
+ run_id: runId
1274
+ }
1275
+ });
1276
+ finalCreatedAt = existing?.createdAt ? ensureDate(existing.createdAt) : /* @__PURE__ */ new Date();
1277
+ }
1278
+ await this.db.insert({
1279
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
1280
+ record: {
1281
+ namespace,
1282
+ workflow_name: workflowName,
1283
+ run_id: runId,
1284
+ resourceId,
1285
+ snapshot,
1286
+ createdAt: finalCreatedAt,
1287
+ updatedAt: updatedAt ?? /* @__PURE__ */ new Date()
1288
+ }
1289
+ });
1290
+ } catch (error) {
1291
+ throw new MastraError({
1292
+ id: createStorageErrorId("REDIS", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
1293
+ domain: ErrorDomain.STORAGE,
1294
+ category: ErrorCategory.THIRD_PARTY,
1295
+ details: {
1296
+ namespace,
1297
+ workflowName,
1298
+ runId
1299
+ }
1300
+ }, error);
1301
+ }
1302
+ }
1303
+ async loadWorkflowSnapshot(params) {
1304
+ const { namespace = "workflows", workflowName, runId } = params;
1305
+ const key = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1306
+ namespace,
1307
+ workflow_name: workflowName,
1308
+ run_id: runId
1309
+ });
1310
+ try {
1311
+ const data = await this.client.get(key);
1312
+ if (!data) return null;
1313
+ return JSON.parse(data).snapshot;
1314
+ } catch (error) {
1315
+ throw new MastraError({
1316
+ id: createStorageErrorId("REDIS", "LOAD_WORKFLOW_SNAPSHOT", "FAILED"),
1317
+ domain: ErrorDomain.STORAGE,
1318
+ category: ErrorCategory.THIRD_PARTY,
1319
+ details: {
1320
+ namespace,
1321
+ workflowName,
1322
+ runId
1323
+ }
1324
+ }, error);
1325
+ }
1326
+ }
1327
+ async getWorkflowRunById({ runId, workflowName }) {
1328
+ try {
1329
+ const key = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1330
+ namespace: "workflows",
1331
+ workflow_name: workflowName,
1332
+ run_id: runId
1333
+ }) + "*";
1334
+ const keys = await this.db.scanKeys(key);
1335
+ if (keys.length === 0) return null;
1336
+ const data = (await this.client.mGet(keys)).filter((data) => data !== null).map((data) => JSON.parse(data)).find((workflow) => {
1337
+ if (!workflow) return false;
1338
+ const runIdMatch = workflow.run_id === runId;
1339
+ if (workflowName) return runIdMatch && workflow.workflow_name === workflowName;
1340
+ return runIdMatch;
1341
+ });
1342
+ if (!data) return null;
1343
+ return parseWorkflowRun(data);
1344
+ } catch (error) {
1345
+ throw new MastraError({
1346
+ id: createStorageErrorId("REDIS", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
1347
+ domain: ErrorDomain.STORAGE,
1348
+ category: ErrorCategory.THIRD_PARTY,
1349
+ details: {
1350
+ namespace: "workflows",
1351
+ runId,
1352
+ workflowName: workflowName || ""
1353
+ }
1354
+ }, error);
1355
+ }
1356
+ }
1357
+ async deleteWorkflowRunById({ runId, workflowName }) {
1358
+ const key = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1359
+ namespace: "workflows",
1360
+ workflow_name: workflowName,
1361
+ run_id: runId
1362
+ });
1363
+ try {
1364
+ await this.client.del(key);
1365
+ } catch (error) {
1366
+ throw new MastraError({
1367
+ id: createStorageErrorId("REDIS", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
1368
+ domain: ErrorDomain.STORAGE,
1369
+ category: ErrorCategory.THIRD_PARTY,
1370
+ details: {
1371
+ namespace: "workflows",
1372
+ runId,
1373
+ workflowName
1374
+ }
1375
+ }, error);
1376
+ }
1377
+ }
1378
+ async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, status } = {}) {
1379
+ try {
1380
+ if (page !== void 0 && page < 0) throw new MastraError({
1381
+ id: createStorageErrorId("REDIS", "LIST_WORKFLOW_RUNS", "INVALID_PAGE"),
1382
+ domain: ErrorDomain.STORAGE,
1383
+ category: ErrorCategory.USER,
1384
+ details: { page }
1385
+ }, /* @__PURE__ */ new Error("page must be >= 0"));
1386
+ const normalizedFrom = fromDate ? ensureDate(fromDate) : void 0;
1387
+ const normalizedTo = toDate ? ensureDate(toDate) : void 0;
1388
+ let pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace: "workflows" }) + ":*";
1389
+ if (workflowName && resourceId) pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1390
+ namespace: "workflows",
1391
+ workflow_name: workflowName,
1392
+ run_id: "*",
1393
+ resourceId
1394
+ });
1395
+ else if (workflowName) pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1396
+ namespace: "workflows",
1397
+ workflow_name: workflowName
1398
+ }) + ":*";
1399
+ else if (resourceId) pattern = getKey(TABLE_WORKFLOW_SNAPSHOT, {
1400
+ namespace: "workflows",
1401
+ workflow_name: "*",
1402
+ run_id: "*",
1403
+ resourceId
1404
+ });
1405
+ const keys = await this.db.scanKeys(pattern);
1406
+ if (keys.length === 0) return {
1407
+ runs: [],
1408
+ total: 0
1409
+ };
1410
+ let runs = (await this.client.mGet(keys)).filter((data) => data !== null).map((data) => JSON.parse(data)).filter((record) => record !== null && record !== void 0 && typeof record === "object" && "workflow_name" in record).filter((record) => !workflowName || record.workflow_name === workflowName).map((w) => parseWorkflowRun(w)).filter((w) => {
1411
+ if (normalizedFrom && w.createdAt < normalizedFrom) return false;
1412
+ if (normalizedTo && w.createdAt > normalizedTo) return false;
1413
+ if (status) {
1414
+ let snapshot = w.snapshot;
1415
+ if (typeof snapshot === "string") try {
1416
+ snapshot = JSON.parse(snapshot);
1417
+ } catch (e) {
1418
+ console.warn(`Failed to parse snapshot for workflow ${w.workflowName}: ${e}`);
1419
+ return false;
1420
+ }
1421
+ return snapshot.status === status;
1422
+ }
1423
+ return true;
1424
+ }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1425
+ const total = runs.length;
1426
+ if (typeof perPage === "number" && typeof page === "number") {
1427
+ const normalizedPerPage = normalizePerPage(perPage, Number.MAX_SAFE_INTEGER);
1428
+ const offset = page * normalizedPerPage;
1429
+ runs = runs.slice(offset, offset + normalizedPerPage);
1430
+ }
1431
+ return {
1432
+ runs,
1433
+ total
1434
+ };
1435
+ } catch (error) {
1436
+ if (error instanceof MastraError) throw error;
1437
+ throw new MastraError({
1438
+ id: createStorageErrorId("REDIS", "LIST_WORKFLOW_RUNS", "FAILED"),
1439
+ domain: ErrorDomain.STORAGE,
1440
+ category: ErrorCategory.THIRD_PARTY,
1441
+ details: {
1442
+ namespace: "workflows",
1443
+ workflowName: workflowName || "",
1444
+ resourceId: resourceId || ""
1445
+ }
1446
+ }, error);
1447
+ }
1448
+ }
1740
1449
  };
1741
-
1742
- // src/storage/utils.ts
1450
+ //#endregion
1451
+ //#region src/storage/utils.ts
1743
1452
  function isClientConfig(config) {
1744
- return "client" in config;
1453
+ return "client" in config;
1745
1454
  }
1746
1455
  function isConnectionStringConfig(config) {
1747
- return "connectionString" in config;
1456
+ return "connectionString" in config;
1748
1457
  }
1749
-
1750
- // src/storage/store.ts
1458
+ //#endregion
1459
+ //#region src/storage/store.ts
1460
+ /**
1461
+ * Redis storage adapter for Mastra.
1462
+ *
1463
+ * Provides storage functionality for direct Redis connections using the official redis package.
1464
+ *
1465
+ * Access domain-specific storage via `getStore()`:
1466
+ *
1467
+ * @example
1468
+ * ```typescript
1469
+ * // Using connection string
1470
+ * const storage = new RedisStore({
1471
+ * id: 'my-store',
1472
+ * connectionString: 'redis://localhost:6379',
1473
+ * });
1474
+ *
1475
+ * // Using host/port
1476
+ * const storage = new RedisStore({
1477
+ * id: 'my-store',
1478
+ * host: 'localhost',
1479
+ * port: 6379,
1480
+ * password: 'secret',
1481
+ * });
1482
+ *
1483
+ * // Access memory domain
1484
+ * const memory = await storage.getStore('memory');
1485
+ * await memory?.saveThread({ thread });
1486
+ *
1487
+ * // Access workflows domain
1488
+ * const workflows = await storage.getStore('workflows');
1489
+ * await workflows?.persistWorkflowSnapshot({ workflowName, runId, snapshot });
1490
+ * ```
1491
+ *
1492
+ * @example
1493
+ * ```typescript
1494
+ * // Using a pre-configured client for advanced features
1495
+ * import { createClient } from 'redis';
1496
+ *
1497
+ * const client = createClient({
1498
+ * url: 'redis://localhost:6379',
1499
+ * socket: {
1500
+ * reconnectStrategy: (retries) => Math.min(retries * 50, 2000),
1501
+ * },
1502
+ * });
1503
+ * await client.connect();
1504
+ *
1505
+ * const storage = new RedisStore({
1506
+ * id: 'my-store',
1507
+ * client,
1508
+ * });
1509
+ * ```
1510
+ */
1751
1511
  var RedisStore = class extends MastraStorage {
1752
- client;
1753
- shouldManageConnection;
1754
- stores;
1755
- constructor(config) {
1756
- super({ id: config.id, name: "Redis", disableInit: config.disableInit });
1757
- const { client, shouldManageConnection } = this.createClient(config);
1758
- this.client = client;
1759
- this.shouldManageConnection = shouldManageConnection;
1760
- this.stores = {
1761
- scores: new ScoresRedis({ client: this.client }),
1762
- workflows: new WorkflowsRedis({ client: this.client }),
1763
- memory: new StoreMemoryRedis({ client: this.client })
1764
- };
1765
- }
1766
- async init() {
1767
- if (this.shouldManageConnection && !this.client.isOpen) {
1768
- await this.client.connect();
1769
- }
1770
- await super.init();
1771
- }
1772
- getClient() {
1773
- return this.client;
1774
- }
1775
- async close() {
1776
- if (this.shouldManageConnection && this.client.isOpen) {
1777
- await this.client.quit();
1778
- }
1779
- }
1780
- createClient(config) {
1781
- if (isClientConfig(config)) {
1782
- return { client: config.client, shouldManageConnection: false };
1783
- }
1784
- if (isConnectionStringConfig(config)) {
1785
- if (!config.connectionString?.trim()) {
1786
- throw new Error("RedisStore: connectionString is required and cannot be empty.");
1787
- }
1788
- return {
1789
- client: createClient({ url: config.connectionString }),
1790
- shouldManageConnection: true
1791
- };
1792
- }
1793
- if (!config.host?.trim()) {
1794
- throw new Error("RedisStore: host is required and cannot be empty.");
1795
- }
1796
- const url = this.createClientUrl({
1797
- ...config,
1798
- db: config.db ?? 0,
1799
- port: config.port ?? 6379
1800
- });
1801
- return {
1802
- client: createClient({ url }),
1803
- shouldManageConnection: true
1804
- };
1805
- }
1806
- createClientUrl(config) {
1807
- const encodedPassword = config.password ? encodeURIComponent(config.password) : null;
1808
- if (config.password) {
1809
- return `redis://:${encodedPassword}@${config.host}:${config.port || 6379}/${config.db || 0}`;
1810
- }
1811
- return `redis://${config.host}:${config.port || 6379}/${config.db || 0}`;
1812
- }
1512
+ client;
1513
+ shouldManageConnection;
1514
+ stores;
1515
+ constructor(config) {
1516
+ super({
1517
+ id: config.id,
1518
+ name: "Redis",
1519
+ disableInit: config.disableInit
1520
+ });
1521
+ const { client, shouldManageConnection } = this.createClient(config);
1522
+ this.client = client;
1523
+ this.shouldManageConnection = shouldManageConnection;
1524
+ this.stores = {
1525
+ scores: new ScoresRedis({ client: this.client }),
1526
+ workflows: new WorkflowsRedis({ client: this.client }),
1527
+ memory: new StoreMemoryRedis({ client: this.client })
1528
+ };
1529
+ }
1530
+ async init() {
1531
+ if (this.shouldManageConnection && !this.client.isOpen) await this.client.connect();
1532
+ await super.init();
1533
+ }
1534
+ getClient() {
1535
+ return this.client;
1536
+ }
1537
+ async close() {
1538
+ if (this.shouldManageConnection && this.client.isOpen) await this.client.quit();
1539
+ }
1540
+ createClient(config) {
1541
+ if (isClientConfig(config)) return {
1542
+ client: config.client,
1543
+ shouldManageConnection: false
1544
+ };
1545
+ if (isConnectionStringConfig(config)) {
1546
+ if (!config.connectionString?.trim()) throw new Error("RedisStore: connectionString is required and cannot be empty.");
1547
+ return {
1548
+ client: createClient({ url: config.connectionString }),
1549
+ shouldManageConnection: true
1550
+ };
1551
+ }
1552
+ if (!config.host?.trim()) throw new Error("RedisStore: host is required and cannot be empty.");
1553
+ return {
1554
+ client: createClient({ url: this.createClientUrl({
1555
+ ...config,
1556
+ db: config.db ?? 0,
1557
+ port: config.port ?? 6379
1558
+ }) }),
1559
+ shouldManageConnection: true
1560
+ };
1561
+ }
1562
+ createClientUrl(config) {
1563
+ const encodedPassword = config.password ? encodeURIComponent(config.password) : null;
1564
+ if (config.password) return `redis://:${encodedPassword}@${config.host}:${config.port || 6379}/${config.db || 0}`;
1565
+ return `redis://${config.host}:${config.port || 6379}/${config.db || 0}`;
1566
+ }
1813
1567
  };
1814
- var defaultSetWithExpiry = (client, key, value, seconds) => {
1815
- return client.set(key, value, "EX", seconds);
1568
+ //#endregion
1569
+ //#region src/cache.ts
1570
+ const defaultSetWithExpiry = (client, key, value, seconds) => {
1571
+ return client.set(key, value, "EX", seconds);
1816
1572
  };
1817
- var defaultScanKeys = (client, cursor, pattern, count) => {
1818
- return client.scan(cursor, "MATCH", pattern, "COUNT", count);
1573
+ const defaultScanKeys = (client, cursor, pattern, count) => {
1574
+ return client.scan(cursor, "MATCH", pattern, "COUNT", count);
1819
1575
  };
1820
- var defaultGetListLength = (client, key) => {
1821
- return client.llen(key);
1576
+ const defaultGetListLength = (client, key) => {
1577
+ return client.llen(key);
1822
1578
  };
1823
- var defaultPushToList = (client, key, value) => {
1824
- return client.rpush(key, value);
1579
+ const defaultPushToList = (client, key, value) => {
1580
+ return client.rpush(key, value);
1825
1581
  };
1826
- var defaultGetListRange = (client, key, start, stop) => {
1827
- return client.lrange(key, start, stop);
1582
+ const defaultGetListRange = (client, key, start, stop) => {
1583
+ return client.lrange(key, start, stop);
1828
1584
  };
1829
1585
  var RedisServerCache = class extends MastraServerCache {
1830
- client;
1831
- keyPrefix;
1832
- ttlSeconds;
1833
- setWithExpiry;
1834
- scanKeys;
1835
- getListLength;
1836
- pushToList;
1837
- getListRange;
1838
- constructor(config, options = {}) {
1839
- super({ name: "RedisServerCache" });
1840
- this.client = config.client;
1841
- this.keyPrefix = options.keyPrefix ?? "mastra:cache:";
1842
- this.ttlSeconds = options.ttlSeconds ?? 300;
1843
- this.setWithExpiry = options.setWithExpiry ?? defaultSetWithExpiry;
1844
- this.scanKeys = options.scanKeys ?? defaultScanKeys;
1845
- this.getListLength = options.getListLength ?? defaultGetListLength;
1846
- this.pushToList = options.pushToList ?? defaultPushToList;
1847
- this.getListRange = options.getListRange ?? defaultGetListRange;
1848
- }
1849
- getKey(key) {
1850
- return `${this.keyPrefix}${key}`;
1851
- }
1852
- serialize(value) {
1853
- return JSON.stringify(value);
1854
- }
1855
- deserialize(value) {
1856
- if (typeof value === "string") {
1857
- try {
1858
- return JSON.parse(value);
1859
- } catch {
1860
- return value;
1861
- }
1862
- }
1863
- return value;
1864
- }
1865
- async get(key) {
1866
- const fullKey = this.getKey(key);
1867
- const value = await this.client.get(fullKey);
1868
- if (value === null) {
1869
- return null;
1870
- }
1871
- return this.deserialize(value);
1872
- }
1873
- async set(key, value, ttlMs) {
1874
- const fullKey = this.getKey(key);
1875
- const serialized = this.serialize(value);
1876
- const overrideSeconds = ttlMs !== void 0 ? Math.max(1, Math.ceil(ttlMs / 1e3)) : void 0;
1877
- const effectiveSeconds = overrideSeconds ?? this.ttlSeconds;
1878
- if (effectiveSeconds > 0) {
1879
- await this.setWithExpiry(this.client, fullKey, serialized, effectiveSeconds);
1880
- } else {
1881
- await this.client.set(fullKey, serialized);
1882
- }
1883
- }
1884
- async listLength(key) {
1885
- const fullKey = this.getKey(key);
1886
- return this.getListLength(this.client, fullKey);
1887
- }
1888
- async listPush(key, value) {
1889
- const fullKey = this.getKey(key);
1890
- const serialized = this.serialize(value);
1891
- await this.pushToList(this.client, fullKey, serialized);
1892
- if (this.ttlSeconds > 0) {
1893
- await this.client.expire(fullKey, this.ttlSeconds);
1894
- }
1895
- }
1896
- async listFromTo(key, from, to = -1) {
1897
- const fullKey = this.getKey(key);
1898
- const values = await this.getListRange(this.client, fullKey, from, to);
1899
- return values.map((v) => this.deserialize(v));
1900
- }
1901
- async delete(key) {
1902
- const fullKey = this.getKey(key);
1903
- await this.client.del(fullKey);
1904
- }
1905
- async clear() {
1906
- const pattern = `${this.keyPrefix}*`;
1907
- let cursor = "0";
1908
- do {
1909
- const [nextCursor, keys] = await this.scanKeys(this.client, cursor, pattern, 100);
1910
- if (keys.length > 0) {
1911
- await this.client.del(...keys);
1912
- }
1913
- cursor = nextCursor;
1914
- } while (cursor !== "0" && cursor !== 0);
1915
- }
1916
- async increment(key) {
1917
- const fullKey = this.getKey(key);
1918
- return this.client.incr(fullKey);
1919
- }
1586
+ client;
1587
+ keyPrefix;
1588
+ ttlSeconds;
1589
+ setWithExpiry;
1590
+ scanKeys;
1591
+ getListLength;
1592
+ pushToList;
1593
+ getListRange;
1594
+ constructor(config, options = {}) {
1595
+ super({ name: "RedisServerCache" });
1596
+ this.client = config.client;
1597
+ this.keyPrefix = options.keyPrefix ?? "mastra:cache:";
1598
+ this.ttlSeconds = options.ttlSeconds ?? 300;
1599
+ this.setWithExpiry = options.setWithExpiry ?? defaultSetWithExpiry;
1600
+ this.scanKeys = options.scanKeys ?? defaultScanKeys;
1601
+ this.getListLength = options.getListLength ?? defaultGetListLength;
1602
+ this.pushToList = options.pushToList ?? defaultPushToList;
1603
+ this.getListRange = options.getListRange ?? defaultGetListRange;
1604
+ }
1605
+ getKey(key) {
1606
+ return `${this.keyPrefix}${key}`;
1607
+ }
1608
+ serialize(value) {
1609
+ return JSON.stringify(value);
1610
+ }
1611
+ deserialize(value) {
1612
+ if (typeof value === "string") try {
1613
+ return JSON.parse(value);
1614
+ } catch {
1615
+ return value;
1616
+ }
1617
+ return value;
1618
+ }
1619
+ async get(key) {
1620
+ const fullKey = this.getKey(key);
1621
+ const value = await this.client.get(fullKey);
1622
+ if (value === null) return null;
1623
+ return this.deserialize(value);
1624
+ }
1625
+ async set(key, value, ttlMs) {
1626
+ const fullKey = this.getKey(key);
1627
+ const serialized = this.serialize(value);
1628
+ const effectiveSeconds = (ttlMs !== void 0 ? Math.max(1, Math.ceil(ttlMs / 1e3)) : void 0) ?? this.ttlSeconds;
1629
+ if (effectiveSeconds > 0) await this.setWithExpiry(this.client, fullKey, serialized, effectiveSeconds);
1630
+ else await this.client.set(fullKey, serialized);
1631
+ }
1632
+ async listLength(key) {
1633
+ const fullKey = this.getKey(key);
1634
+ return this.getListLength(this.client, fullKey);
1635
+ }
1636
+ async listPush(key, value) {
1637
+ const fullKey = this.getKey(key);
1638
+ const serialized = this.serialize(value);
1639
+ await this.pushToList(this.client, fullKey, serialized);
1640
+ if (this.ttlSeconds > 0) await this.client.expire(fullKey, this.ttlSeconds);
1641
+ }
1642
+ async listFromTo(key, from, to = -1) {
1643
+ const fullKey = this.getKey(key);
1644
+ return (await this.getListRange(this.client, fullKey, from, to)).map((v) => this.deserialize(v));
1645
+ }
1646
+ async delete(key) {
1647
+ const fullKey = this.getKey(key);
1648
+ await this.client.del(fullKey);
1649
+ }
1650
+ async clear() {
1651
+ const pattern = `${this.keyPrefix}*`;
1652
+ let cursor = "0";
1653
+ do {
1654
+ const [nextCursor, keys] = await this.scanKeys(this.client, cursor, pattern, 100);
1655
+ if (keys.length > 0) await this.client.del(...keys);
1656
+ cursor = nextCursor;
1657
+ } while (cursor !== "0" && cursor !== 0);
1658
+ }
1659
+ async increment(key) {
1660
+ const fullKey = this.getKey(key);
1661
+ return this.client.incr(fullKey);
1662
+ }
1920
1663
  };
1921
- var upstashPreset = {
1922
- setWithExpiry: (client, key, value, seconds) => client.set(key, value, { ex: seconds }),
1923
- scanKeys: (client, cursor, pattern, count) => client.scan(cursor, { match: pattern, count })
1664
+ const upstashPreset = {
1665
+ setWithExpiry: (client, key, value, seconds) => client.set(key, value, { ex: seconds }),
1666
+ scanKeys: (client, cursor, pattern, count) => client.scan(cursor, {
1667
+ match: pattern,
1668
+ count
1669
+ })
1924
1670
  };
1925
- var nodeRedisPreset = {
1926
- setWithExpiry: (client, key, value, seconds) => client.set(key, value, { EX: seconds }),
1927
- scanKeys: (client, cursor, pattern, count) => client.scan(cursor, { MATCH: pattern, COUNT: count }),
1928
- getListLength: (client, key) => client.lLen(key),
1929
- pushToList: (client, key, value) => client.rPush(key, value),
1930
- getListRange: (client, key, start, stop) => client.lRange(key, start, stop)
1671
+ const nodeRedisPreset = {
1672
+ setWithExpiry: (client, key, value, seconds) => client.set(key, value, { EX: seconds }),
1673
+ scanKeys: (client, cursor, pattern, count) => client.scan(cursor, {
1674
+ MATCH: pattern,
1675
+ COUNT: count
1676
+ }),
1677
+ getListLength: (client, key) => client.lLen(key),
1678
+ pushToList: (client, key, value) => client.rPush(key, value),
1679
+ getListRange: (client, key, start, stop) => client.lRange(key, start, stop)
1931
1680
  };
1932
-
1681
+ //#endregion
1933
1682
  export { RedisServerCache, RedisStore, ScoresRedis, StoreMemoryRedis, WorkflowsRedis, nodeRedisPreset, upstashPreset };
1934
- //# sourceMappingURL=index.js.map
1683
+
1935
1684
  //# sourceMappingURL=index.js.map