@mastra/upstash 0.10.0 → 0.10.1-alpha.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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +9 -0
- package/dist/_tsup-dts-rollup.d.cts +27 -14
- package/dist/_tsup-dts-rollup.d.ts +27 -14
- package/dist/index.cjs +181 -126
- package/dist/index.js +182 -127
- package/package.json +3 -3
- package/src/storage/index.ts +217 -156
- package/src/storage/upstash.test.ts +21 -22
package/src/storage/index.ts
CHANGED
|
@@ -25,15 +25,189 @@ export interface UpstashConfig {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export class UpstashStore extends MastraStorage {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
private redis: Redis;
|
|
29
|
+
|
|
30
|
+
constructor(config: UpstashConfig) {
|
|
31
|
+
super({ name: 'Upstash' });
|
|
32
|
+
this.redis = new Redis({
|
|
33
|
+
url: config.url,
|
|
34
|
+
token: config.token,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private transformEvalRecord(record: Record<string, any>): EvalRow {
|
|
39
|
+
// Parse JSON strings if needed
|
|
40
|
+
let result = record.result;
|
|
41
|
+
if (typeof result === 'string') {
|
|
42
|
+
try {
|
|
43
|
+
result = JSON.parse(result);
|
|
44
|
+
} catch {
|
|
45
|
+
console.warn('Failed to parse result JSON:');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let testInfo = record.test_info;
|
|
50
|
+
if (typeof testInfo === 'string') {
|
|
51
|
+
try {
|
|
52
|
+
testInfo = JSON.parse(testInfo);
|
|
53
|
+
} catch {
|
|
54
|
+
console.warn('Failed to parse test_info JSON:');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
agentName: record.agent_name,
|
|
60
|
+
input: record.input,
|
|
61
|
+
output: record.output,
|
|
62
|
+
result: result as MetricResult,
|
|
63
|
+
metricName: record.metric_name,
|
|
64
|
+
instructions: record.instructions,
|
|
65
|
+
testInfo: testInfo as TestInfo | undefined,
|
|
66
|
+
globalRunId: record.global_run_id,
|
|
67
|
+
runId: record.run_id,
|
|
68
|
+
createdAt:
|
|
69
|
+
typeof record.created_at === 'string'
|
|
70
|
+
? record.created_at
|
|
71
|
+
: record.created_at instanceof Date
|
|
72
|
+
? record.created_at.toISOString()
|
|
73
|
+
: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private parseJSON(value: any): any {
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(value);
|
|
81
|
+
} catch {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private getKey(tableName: TABLE_NAMES, keys: Record<string, any>): string {
|
|
89
|
+
const keyParts = Object.entries(keys)
|
|
90
|
+
.filter(([_, value]) => value !== undefined)
|
|
91
|
+
.map(([key, value]) => `${key}:${value}`);
|
|
92
|
+
return `${tableName}:${keyParts.join(':')}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private ensureDate(date: Date | string | undefined): Date | undefined {
|
|
96
|
+
if (!date) return undefined;
|
|
97
|
+
return date instanceof Date ? date : new Date(date);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private serializeDate(date: Date | string | undefined): string | undefined {
|
|
101
|
+
if (!date) return undefined;
|
|
102
|
+
const dateObj = this.ensureDate(date);
|
|
103
|
+
return dateObj?.toISOString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Scans for keys matching the given pattern using SCAN and returns them as an array.
|
|
108
|
+
* @param pattern Redis key pattern, e.g. "table:*"
|
|
109
|
+
* @param batchSize Number of keys to scan per batch (default: 1000)
|
|
110
|
+
*/
|
|
111
|
+
private async scanKeys(pattern: string, batchSize = 10000): Promise<string[]> {
|
|
112
|
+
let cursor = '0';
|
|
113
|
+
let keys: string[] = [];
|
|
114
|
+
do {
|
|
115
|
+
// Upstash: scan(cursor, { match, count })
|
|
116
|
+
const [nextCursor, batch] = await this.redis.scan(cursor, {
|
|
117
|
+
match: pattern,
|
|
118
|
+
count: batchSize,
|
|
119
|
+
});
|
|
120
|
+
keys.push(...batch);
|
|
121
|
+
cursor = nextCursor;
|
|
122
|
+
} while (cursor !== '0');
|
|
123
|
+
return keys;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Deletes all keys matching the given pattern using SCAN and DEL in batches.
|
|
128
|
+
* @param pattern Redis key pattern, e.g. "table:*"
|
|
129
|
+
* @param batchSize Number of keys to delete per batch (default: 1000)
|
|
130
|
+
*/
|
|
131
|
+
private async scanAndDelete(pattern: string, batchSize = 10000): Promise<number> {
|
|
132
|
+
let cursor = '0';
|
|
133
|
+
let totalDeleted = 0;
|
|
134
|
+
do {
|
|
135
|
+
const [nextCursor, keys] = await this.redis.scan(cursor, {
|
|
136
|
+
match: pattern,
|
|
137
|
+
count: batchSize,
|
|
138
|
+
});
|
|
139
|
+
if (keys.length > 0) {
|
|
140
|
+
await this.redis.del(...keys);
|
|
141
|
+
totalDeleted += keys.length;
|
|
142
|
+
}
|
|
143
|
+
cursor = nextCursor;
|
|
144
|
+
} while (cursor !== '0');
|
|
145
|
+
return totalDeleted;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private getMessageKey(threadId: string, messageId: string): string {
|
|
149
|
+
return this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private getThreadMessagesKey(threadId: string): string {
|
|
153
|
+
return `thread:${threadId}:messages`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private parseWorkflowRun(row: any): WorkflowRun {
|
|
157
|
+
let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
|
|
158
|
+
if (typeof parsedSnapshot === 'string') {
|
|
159
|
+
try {
|
|
160
|
+
parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// If parsing fails, return the raw snapshot string
|
|
163
|
+
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
workflowName: row.workflow_name,
|
|
169
|
+
runId: row.run_id,
|
|
170
|
+
snapshot: parsedSnapshot,
|
|
171
|
+
createdAt: this.ensureDate(row.createdAt)!,
|
|
172
|
+
updatedAt: this.ensureDate(row.updatedAt)!,
|
|
173
|
+
resourceId: row.resourceId,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private processRecord(tableName: TABLE_NAMES, record: Record<string, any>) {
|
|
178
|
+
let key: string;
|
|
179
|
+
|
|
180
|
+
if (tableName === TABLE_MESSAGES) {
|
|
181
|
+
// For messages, use threadId as the primary key component
|
|
182
|
+
key = this.getKey(tableName, { threadId: record.threadId, id: record.id });
|
|
183
|
+
} else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
184
|
+
key = this.getKey(tableName, {
|
|
185
|
+
namespace: record.namespace || 'workflows',
|
|
186
|
+
workflow_name: record.workflow_name,
|
|
187
|
+
run_id: record.run_id,
|
|
188
|
+
...(record.resourceId ? { resourceId: record.resourceId } : {}),
|
|
189
|
+
});
|
|
190
|
+
} else if (tableName === TABLE_EVALS) {
|
|
191
|
+
key = this.getKey(tableName, { id: record.run_id });
|
|
192
|
+
} else {
|
|
193
|
+
key = this.getKey(tableName, { id: record.id });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Convert dates to ISO strings before storing
|
|
197
|
+
const processedRecord = {
|
|
198
|
+
...record,
|
|
199
|
+
createdAt: this.serializeDate(record.createdAt),
|
|
200
|
+
updatedAt: this.serializeDate(record.updatedAt),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return { key, processedRecord };
|
|
30
204
|
}
|
|
31
205
|
|
|
32
206
|
async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
|
|
33
207
|
try {
|
|
34
208
|
// Get all keys that match the evals table pattern
|
|
35
209
|
const pattern = `${TABLE_EVALS}:*`;
|
|
36
|
-
const keys = await this.
|
|
210
|
+
const keys = await this.scanKeys(pattern);
|
|
37
211
|
|
|
38
212
|
// Fetch all eval records
|
|
39
213
|
const evalRecords = await Promise.all(
|
|
@@ -96,45 +270,6 @@ export class UpstashStore extends MastraStorage {
|
|
|
96
270
|
}
|
|
97
271
|
}
|
|
98
272
|
|
|
99
|
-
private transformEvalRecord(record: Record<string, any>): EvalRow {
|
|
100
|
-
// Parse JSON strings if needed
|
|
101
|
-
let result = record.result;
|
|
102
|
-
if (typeof result === 'string') {
|
|
103
|
-
try {
|
|
104
|
-
result = JSON.parse(result);
|
|
105
|
-
} catch {
|
|
106
|
-
console.warn('Failed to parse result JSON:');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let testInfo = record.test_info;
|
|
111
|
-
if (typeof testInfo === 'string') {
|
|
112
|
-
try {
|
|
113
|
-
testInfo = JSON.parse(testInfo);
|
|
114
|
-
} catch {
|
|
115
|
-
console.warn('Failed to parse test_info JSON:');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
agentName: record.agent_name,
|
|
121
|
-
input: record.input,
|
|
122
|
-
output: record.output,
|
|
123
|
-
result: result as MetricResult,
|
|
124
|
-
metricName: record.metric_name,
|
|
125
|
-
instructions: record.instructions,
|
|
126
|
-
testInfo: testInfo as TestInfo | undefined,
|
|
127
|
-
globalRunId: record.global_run_id,
|
|
128
|
-
runId: record.run_id,
|
|
129
|
-
createdAt:
|
|
130
|
-
typeof record.created_at === 'string'
|
|
131
|
-
? record.created_at
|
|
132
|
-
: record.created_at instanceof Date
|
|
133
|
-
? record.created_at.toISOString()
|
|
134
|
-
: new Date().toISOString(),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
273
|
async getTraces(
|
|
139
274
|
{
|
|
140
275
|
name,
|
|
@@ -162,7 +297,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
162
297
|
try {
|
|
163
298
|
// Get all keys that match the traces table pattern
|
|
164
299
|
const pattern = `${TABLE_TRACES}:*`;
|
|
165
|
-
const keys = await this.
|
|
300
|
+
const keys = await this.scanKeys(pattern);
|
|
166
301
|
|
|
167
302
|
// Fetch all trace records
|
|
168
303
|
const traceRecords = await Promise.all(
|
|
@@ -253,45 +388,6 @@ export class UpstashStore extends MastraStorage {
|
|
|
253
388
|
}
|
|
254
389
|
}
|
|
255
390
|
|
|
256
|
-
private parseJSON(value: any): any {
|
|
257
|
-
if (typeof value === 'string') {
|
|
258
|
-
try {
|
|
259
|
-
return JSON.parse(value);
|
|
260
|
-
} catch {
|
|
261
|
-
return value;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return value;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private redis: Redis;
|
|
268
|
-
|
|
269
|
-
constructor(config: UpstashConfig) {
|
|
270
|
-
super({ name: 'Upstash' });
|
|
271
|
-
this.redis = new Redis({
|
|
272
|
-
url: config.url,
|
|
273
|
-
token: config.token,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private getKey(tableName: TABLE_NAMES, keys: Record<string, any>): string {
|
|
278
|
-
const keyParts = Object.entries(keys)
|
|
279
|
-
.filter(([_, value]) => value !== undefined)
|
|
280
|
-
.map(([key, value]) => `${key}:${value}`);
|
|
281
|
-
return `${tableName}:${keyParts.join(':')}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
private ensureDate(date: Date | string | undefined): Date | undefined {
|
|
285
|
-
if (!date) return undefined;
|
|
286
|
-
return date instanceof Date ? date : new Date(date);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private serializeDate(date: Date | string | undefined): string | undefined {
|
|
290
|
-
if (!date) return undefined;
|
|
291
|
-
const dateObj = this.ensureDate(date);
|
|
292
|
-
return dateObj?.toISOString();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
391
|
async createTable({
|
|
296
392
|
tableName,
|
|
297
393
|
schema,
|
|
@@ -306,41 +402,31 @@ export class UpstashStore extends MastraStorage {
|
|
|
306
402
|
|
|
307
403
|
async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
308
404
|
const pattern = `${tableName}:*`;
|
|
309
|
-
|
|
310
|
-
if (keys.length > 0) {
|
|
311
|
-
await this.redis.del(...keys);
|
|
312
|
-
}
|
|
405
|
+
await this.scanAndDelete(pattern);
|
|
313
406
|
}
|
|
314
407
|
|
|
315
408
|
async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (tableName === TABLE_MESSAGES) {
|
|
319
|
-
// For messages, use threadId as the primary key component
|
|
320
|
-
key = this.getKey(tableName, { threadId: record.threadId, id: record.id });
|
|
321
|
-
} else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
322
|
-
key = this.getKey(tableName, {
|
|
323
|
-
namespace: record.namespace || 'workflows',
|
|
324
|
-
workflow_name: record.workflow_name,
|
|
325
|
-
run_id: record.run_id,
|
|
326
|
-
...(record.resourceId ? { resourceId: record.resourceId } : {}),
|
|
327
|
-
});
|
|
328
|
-
} else if (tableName === TABLE_EVALS) {
|
|
329
|
-
key = this.getKey(tableName, { id: record.run_id });
|
|
330
|
-
} else {
|
|
331
|
-
key = this.getKey(tableName, { id: record.id });
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Convert dates to ISO strings before storing
|
|
335
|
-
const processedRecord = {
|
|
336
|
-
...record,
|
|
337
|
-
createdAt: this.serializeDate(record.createdAt),
|
|
338
|
-
updatedAt: this.serializeDate(record.updatedAt),
|
|
339
|
-
};
|
|
409
|
+
const { key, processedRecord } = this.processRecord(tableName, record);
|
|
340
410
|
|
|
341
411
|
await this.redis.set(key, processedRecord);
|
|
342
412
|
}
|
|
343
413
|
|
|
414
|
+
async batchInsert(input: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
|
|
415
|
+
const { tableName, records } = input;
|
|
416
|
+
if (!records.length) return;
|
|
417
|
+
|
|
418
|
+
const batchSize = 1000;
|
|
419
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
420
|
+
const batch = records.slice(i, i + batchSize);
|
|
421
|
+
const pipeline = this.redis.pipeline();
|
|
422
|
+
for (const record of batch) {
|
|
423
|
+
const { key, processedRecord } = this.processRecord(tableName, record);
|
|
424
|
+
pipeline.set(key, processedRecord);
|
|
425
|
+
}
|
|
426
|
+
await pipeline.exec();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
344
430
|
async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
|
|
345
431
|
const key = this.getKey(tableName, keys);
|
|
346
432
|
const data = await this.redis.get<R>(key);
|
|
@@ -365,7 +451,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
365
451
|
|
|
366
452
|
async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
|
|
367
453
|
const pattern = `${TABLE_THREADS}:*`;
|
|
368
|
-
const keys = await this.
|
|
454
|
+
const keys = await this.scanKeys(pattern);
|
|
369
455
|
const threads = await Promise.all(
|
|
370
456
|
keys.map(async key => {
|
|
371
457
|
const data = await this.redis.get<StorageThreadType>(key);
|
|
@@ -423,40 +509,36 @@ export class UpstashStore extends MastraStorage {
|
|
|
423
509
|
await this.redis.del(key);
|
|
424
510
|
}
|
|
425
511
|
|
|
426
|
-
private getMessageKey(threadId: string, messageId: string): string {
|
|
427
|
-
return this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private getThreadMessagesKey(threadId: string): string {
|
|
431
|
-
return `thread:${threadId}:messages`;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
512
|
async saveMessages({ messages }: { messages: MessageType[] }): Promise<MessageType[]> {
|
|
435
513
|
if (messages.length === 0) return [];
|
|
436
514
|
|
|
437
|
-
const pipeline = this.redis.pipeline();
|
|
438
|
-
|
|
439
515
|
// Add an index to each message to maintain order
|
|
440
516
|
const messagesWithIndex = messages.map((message, index) => ({
|
|
441
517
|
...message,
|
|
442
518
|
_index: index,
|
|
443
519
|
}));
|
|
444
520
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
521
|
+
const batchSize = 1000;
|
|
522
|
+
for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
|
|
523
|
+
const batch = messagesWithIndex.slice(i, i + batchSize);
|
|
524
|
+
const pipeline = this.redis.pipeline();
|
|
525
|
+
for (const message of batch) {
|
|
526
|
+
const key = this.getMessageKey(message.threadId, message.id);
|
|
527
|
+
const score = message._index !== undefined ? message._index : new Date(message.createdAt).getTime();
|
|
528
|
+
|
|
529
|
+
// Store the message data
|
|
530
|
+
pipeline.set(key, message);
|
|
531
|
+
|
|
532
|
+
// Add to sorted set for this thread
|
|
533
|
+
pipeline.zadd(this.getThreadMessagesKey(message.threadId), {
|
|
534
|
+
score,
|
|
535
|
+
member: message.id,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
451
538
|
|
|
452
|
-
|
|
453
|
-
pipeline.zadd(this.getThreadMessagesKey(message.threadId), {
|
|
454
|
-
score,
|
|
455
|
-
member: message.id,
|
|
456
|
-
});
|
|
539
|
+
await pipeline.exec();
|
|
457
540
|
}
|
|
458
541
|
|
|
459
|
-
await pipeline.exec();
|
|
460
542
|
return messages;
|
|
461
543
|
}
|
|
462
544
|
|
|
@@ -557,27 +639,6 @@ export class UpstashStore extends MastraStorage {
|
|
|
557
639
|
return data.snapshot;
|
|
558
640
|
}
|
|
559
641
|
|
|
560
|
-
private parseWorkflowRun(row: any): WorkflowRun {
|
|
561
|
-
let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
|
|
562
|
-
if (typeof parsedSnapshot === 'string') {
|
|
563
|
-
try {
|
|
564
|
-
parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
|
|
565
|
-
} catch (e) {
|
|
566
|
-
// If parsing fails, return the raw snapshot string
|
|
567
|
-
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return {
|
|
572
|
-
workflowName: row.workflow_name,
|
|
573
|
-
runId: row.run_id,
|
|
574
|
-
snapshot: parsedSnapshot,
|
|
575
|
-
createdAt: this.ensureDate(row.createdAt)!,
|
|
576
|
-
updatedAt: this.ensureDate(row.updatedAt)!,
|
|
577
|
-
resourceId: row.resourceId,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
642
|
async getWorkflowRuns(
|
|
582
643
|
{
|
|
583
644
|
namespace,
|
|
@@ -612,7 +673,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
612
673
|
} else if (resourceId) {
|
|
613
674
|
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: '*', run_id: '*', resourceId });
|
|
614
675
|
}
|
|
615
|
-
const keys = await this.
|
|
676
|
+
const keys = await this.scanKeys(pattern);
|
|
616
677
|
|
|
617
678
|
// Get all workflow data
|
|
618
679
|
const workflows = await Promise.all(
|
|
@@ -665,7 +726,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
665
726
|
}): Promise<WorkflowRun | null> {
|
|
666
727
|
try {
|
|
667
728
|
const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + '*';
|
|
668
|
-
const keys = await this.
|
|
729
|
+
const keys = await this.scanKeys(key);
|
|
669
730
|
const workflows = await Promise.all(
|
|
670
731
|
keys.map(async key => {
|
|
671
732
|
const data = await this.redis.get<{
|
|
@@ -14,7 +14,7 @@ import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vites
|
|
|
14
14
|
import { UpstashStore } from './index';
|
|
15
15
|
|
|
16
16
|
// Increase timeout for all tests in this file to 30 seconds
|
|
17
|
-
vi.setConfig({ testTimeout:
|
|
17
|
+
vi.setConfig({ testTimeout: 200_000, hookTimeout: 200_000 });
|
|
18
18
|
|
|
19
19
|
const createSampleThread = (date?: Date) => ({
|
|
20
20
|
id: `thread-${randomUUID()}`,
|
|
@@ -42,16 +42,13 @@ const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
|
|
|
42
42
|
const snapshot: WorkflowRunState = {
|
|
43
43
|
value: {},
|
|
44
44
|
context: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
error: undefined,
|
|
50
|
-
},
|
|
45
|
+
[stepId]: {
|
|
46
|
+
status: status,
|
|
47
|
+
payload: {},
|
|
48
|
+
error: undefined,
|
|
51
49
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
},
|
|
50
|
+
input: {},
|
|
51
|
+
} as WorkflowRunState['context'],
|
|
55
52
|
activePaths: [],
|
|
56
53
|
suspendedPaths: {},
|
|
57
54
|
runId,
|
|
@@ -98,7 +95,7 @@ const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: stri
|
|
|
98
95
|
if (typeof snapshot === 'string') {
|
|
99
96
|
throw new Error('Expected WorkflowRunState, got string');
|
|
100
97
|
}
|
|
101
|
-
expect(snapshot.context?.
|
|
98
|
+
expect(snapshot.context?.[stepId]?.status).toBe(status);
|
|
102
99
|
};
|
|
103
100
|
|
|
104
101
|
describe('UpstashStore', () => {
|
|
@@ -227,6 +224,16 @@ describe('UpstashStore', () => {
|
|
|
227
224
|
updated: 'value',
|
|
228
225
|
});
|
|
229
226
|
});
|
|
227
|
+
it('should fetch >100000 threads by resource ID', async () => {
|
|
228
|
+
const resourceId = `resource-${randomUUID()}`;
|
|
229
|
+
const total = 100_000;
|
|
230
|
+
const threads = Array.from({ length: total }, () => ({ ...createSampleThread(), resourceId }));
|
|
231
|
+
|
|
232
|
+
await store.batchInsert({ tableName: TABLE_THREADS, records: threads });
|
|
233
|
+
|
|
234
|
+
const retrievedThreads = await store.getThreadsByResourceId({ resourceId });
|
|
235
|
+
expect(retrievedThreads).toHaveLength(total);
|
|
236
|
+
});
|
|
230
237
|
});
|
|
231
238
|
|
|
232
239
|
describe('Date Handling', () => {
|
|
@@ -455,13 +462,8 @@ describe('UpstashStore', () => {
|
|
|
455
462
|
const mockSnapshot = {
|
|
456
463
|
value: { step1: 'completed' },
|
|
457
464
|
context: {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
},
|
|
461
|
-
steps: {},
|
|
462
|
-
attempts: {},
|
|
463
|
-
triggerData: {},
|
|
464
|
-
},
|
|
465
|
+
step1: { status: 'success', output: { result: 'done' } },
|
|
466
|
+
} as WorkflowRunState['context'],
|
|
465
467
|
runId: testRunId,
|
|
466
468
|
activePaths: [],
|
|
467
469
|
suspendedPaths: {},
|
|
@@ -619,10 +621,7 @@ describe('UpstashStore', () => {
|
|
|
619
621
|
expect(total).toBe(1);
|
|
620
622
|
expect(runs[0]!.workflowName).toBe(workflowName1);
|
|
621
623
|
const snapshot = runs[0]!.snapshot;
|
|
622
|
-
|
|
623
|
-
throw new Error('Expected WorkflowRunState, got string');
|
|
624
|
-
}
|
|
625
|
-
expect(snapshot.context?.steps[stepId1]?.status).toBe('success');
|
|
624
|
+
checkWorkflowSnapshot(snapshot, stepId1, 'success');
|
|
626
625
|
});
|
|
627
626
|
|
|
628
627
|
it('filters by date range', async () => {
|