@mastra/upstash 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +26 -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 +28 -21
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,15 @@ const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
|
|
|
42
42
|
const snapshot: WorkflowRunState = {
|
|
43
43
|
value: {},
|
|
44
44
|
context: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
[stepId]: {
|
|
46
|
+
status: status,
|
|
47
|
+
payload: {},
|
|
48
|
+
error: undefined,
|
|
49
|
+
startedAt: timestamp.getTime(),
|
|
50
|
+
endedAt: new Date(timestamp.getTime() + 15000).getTime(),
|
|
51
51
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
},
|
|
52
|
+
input: {},
|
|
53
|
+
} as WorkflowRunState['context'],
|
|
55
54
|
activePaths: [],
|
|
56
55
|
suspendedPaths: {},
|
|
57
56
|
runId,
|
|
@@ -98,7 +97,7 @@ const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: stri
|
|
|
98
97
|
if (typeof snapshot === 'string') {
|
|
99
98
|
throw new Error('Expected WorkflowRunState, got string');
|
|
100
99
|
}
|
|
101
|
-
expect(snapshot.context?.
|
|
100
|
+
expect(snapshot.context?.[stepId]?.status).toBe(status);
|
|
102
101
|
};
|
|
103
102
|
|
|
104
103
|
describe('UpstashStore', () => {
|
|
@@ -227,6 +226,16 @@ describe('UpstashStore', () => {
|
|
|
227
226
|
updated: 'value',
|
|
228
227
|
});
|
|
229
228
|
});
|
|
229
|
+
it('should fetch >100000 threads by resource ID', async () => {
|
|
230
|
+
const resourceId = `resource-${randomUUID()}`;
|
|
231
|
+
const total = 100_000;
|
|
232
|
+
const threads = Array.from({ length: total }, () => ({ ...createSampleThread(), resourceId }));
|
|
233
|
+
|
|
234
|
+
await store.batchInsert({ tableName: TABLE_THREADS, records: threads });
|
|
235
|
+
|
|
236
|
+
const retrievedThreads = await store.getThreadsByResourceId({ resourceId });
|
|
237
|
+
expect(retrievedThreads).toHaveLength(total);
|
|
238
|
+
});
|
|
230
239
|
});
|
|
231
240
|
|
|
232
241
|
describe('Date Handling', () => {
|
|
@@ -455,13 +464,14 @@ describe('UpstashStore', () => {
|
|
|
455
464
|
const mockSnapshot = {
|
|
456
465
|
value: { step1: 'completed' },
|
|
457
466
|
context: {
|
|
458
|
-
|
|
459
|
-
|
|
467
|
+
step1: {
|
|
468
|
+
status: 'success',
|
|
469
|
+
output: { result: 'done' },
|
|
470
|
+
payload: {},
|
|
471
|
+
startedAt: new Date().getTime(),
|
|
472
|
+
endedAt: new Date(Date.now() + 15000).getTime(),
|
|
460
473
|
},
|
|
461
|
-
|
|
462
|
-
attempts: {},
|
|
463
|
-
triggerData: {},
|
|
464
|
-
},
|
|
474
|
+
} as WorkflowRunState['context'],
|
|
465
475
|
runId: testRunId,
|
|
466
476
|
activePaths: [],
|
|
467
477
|
suspendedPaths: {},
|
|
@@ -619,10 +629,7 @@ describe('UpstashStore', () => {
|
|
|
619
629
|
expect(total).toBe(1);
|
|
620
630
|
expect(runs[0]!.workflowName).toBe(workflowName1);
|
|
621
631
|
const snapshot = runs[0]!.snapshot;
|
|
622
|
-
|
|
623
|
-
throw new Error('Expected WorkflowRunState, got string');
|
|
624
|
-
}
|
|
625
|
-
expect(snapshot.context?.steps[stepId1]?.status).toBe('success');
|
|
632
|
+
checkWorkflowSnapshot(snapshot, stepId1, 'success');
|
|
626
633
|
});
|
|
627
634
|
|
|
628
635
|
it('filters by date range', async () => {
|