@mastra/cloudflare 1.0.0-beta.6 → 1.0.0-beta.8

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,1512 +1,1581 @@
1
1
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
2
- import { MastraStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT, TABLE_SCORERS, createStorageErrorId, StoreOperations, TABLE_TRACES, WorkflowsStorage, ensureDate, normalizePerPage, MemoryStorage, calculatePagination, serializeDate, TABLE_RESOURCES, ScoresStorage, transformScoreRow as transformScoreRow$1 } from '@mastra/core/storage';
2
+ import { MastraStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT, TABLE_SCORERS, createStorageErrorId, WorkflowsStorage, ensureDate, normalizePerPage, MemoryStorage, TABLE_RESOURCES, calculatePagination, serializeDate, ScoresStorage, TABLE_TRACES, transformScoreRow as transformScoreRow$1 } from '@mastra/core/storage';
3
3
  import Cloudflare from 'cloudflare';
4
4
  import { MessageList } from '@mastra/core/agent';
5
+ import { MastraBase } from '@mastra/core/base';
5
6
  import { saveScorePayloadSchema } from '@mastra/core/evals';
6
7
 
7
8
  // src/storage/index.ts
8
- var MemoryStorageCloudflare = class extends MemoryStorage {
9
- operations;
10
- constructor({ operations }) {
11
- super();
12
- this.operations = operations;
9
+ function resolveCloudflareConfig(config) {
10
+ if ("client" in config) {
11
+ return {
12
+ client: config.client,
13
+ accountId: config.accountId,
14
+ namespacePrefix: config.namespacePrefix
15
+ };
13
16
  }
14
- ensureMetadata(metadata) {
15
- if (!metadata) return void 0;
16
- return typeof metadata === "string" ? JSON.parse(metadata) : metadata;
17
+ if ("bindings" in config) {
18
+ return {
19
+ bindings: config.bindings,
20
+ namespacePrefix: config.keyPrefix
21
+ };
22
+ }
23
+ return {
24
+ client: new Cloudflare({ apiToken: config.apiToken }),
25
+ accountId: config.accountId,
26
+ namespacePrefix: config.namespacePrefix
27
+ };
28
+ }
29
+ var CloudflareKVDB = class extends MastraBase {
30
+ bindings;
31
+ client;
32
+ accountId;
33
+ namespacePrefix;
34
+ constructor(config) {
35
+ super({
36
+ component: "STORAGE",
37
+ name: "CLOUDFLARE_KV_DB"
38
+ });
39
+ this.bindings = config.bindings;
40
+ this.namespacePrefix = config.namespacePrefix || "";
41
+ this.client = config.client;
42
+ this.accountId = config.accountId;
43
+ }
44
+ getBinding(tableName) {
45
+ if (!this.bindings) {
46
+ throw new Error(`Cannot use Workers API binding for ${tableName}: Store initialized with REST API configuration`);
47
+ }
48
+ const binding = this.bindings[tableName];
49
+ if (!binding) throw new Error(`No binding found for namespace ${tableName}`);
50
+ return binding;
51
+ }
52
+ getKey(tableName, record) {
53
+ const prefix = this.namespacePrefix ? `${this.namespacePrefix}:` : "";
54
+ switch (tableName) {
55
+ case TABLE_THREADS:
56
+ if (!record.id) throw new Error("Thread ID is required");
57
+ return `${prefix}${tableName}:${record.id}`;
58
+ case TABLE_MESSAGES:
59
+ if (!record.threadId || !record.id) throw new Error("Thread ID and Message ID are required");
60
+ return `${prefix}${tableName}:${record.threadId}:${record.id}`;
61
+ case TABLE_WORKFLOW_SNAPSHOT:
62
+ if (!record.workflow_name || !record.run_id) {
63
+ throw new Error("Workflow name, and run ID are required");
64
+ }
65
+ let key = `${prefix}${tableName}:${record.workflow_name}:${record.run_id}`;
66
+ if (record.resourceId) {
67
+ key = `${key}:${record.resourceId}`;
68
+ }
69
+ return key;
70
+ case TABLE_TRACES:
71
+ if (!record.id) throw new Error("Trace ID is required");
72
+ return `${prefix}${tableName}:${record.id}`;
73
+ case TABLE_SCORERS:
74
+ if (!record.id) throw new Error("Score ID is required");
75
+ return `${prefix}${tableName}:${record.id}`;
76
+ default:
77
+ throw new Error(`Unsupported table: ${tableName}`);
78
+ }
79
+ }
80
+ getSchemaKey(tableName) {
81
+ const prefix = this.namespacePrefix ? `${this.namespacePrefix}:` : "";
82
+ return `${prefix}schema:${tableName}`;
17
83
  }
18
84
  /**
19
- * Summarizes message content without exposing raw data (for logging).
20
- * Returns type, length, and keys only to prevent PII leakage.
85
+ * Helper to safely parse data from KV storage
21
86
  */
22
- summarizeMessageContent(content) {
23
- if (!content) return { type: "undefined" };
24
- if (typeof content === "string") return { type: "string", length: content.length };
25
- if (Array.isArray(content)) return { type: "array", length: content.length };
26
- if (typeof content === "object") return { type: "object", keys: Object.keys(content) };
27
- return { type: typeof content };
28
- }
29
- async getThreadById({ threadId }) {
30
- const thread = await this.operations.load({ tableName: TABLE_THREADS, keys: { id: threadId } });
31
- if (!thread) return null;
87
+ safeParse(text) {
88
+ if (!text) return null;
32
89
  try {
33
- return {
34
- ...thread,
35
- createdAt: ensureDate(thread.createdAt),
36
- updatedAt: ensureDate(thread.updatedAt),
37
- metadata: this.ensureMetadata(thread.metadata)
38
- };
39
- } catch (error) {
40
- const mastraError = new MastraError(
41
- {
42
- id: createStorageErrorId("CLOUDFLARE", "GET_THREAD_BY_ID", "FAILED"),
43
- domain: ErrorDomain.STORAGE,
44
- category: ErrorCategory.THIRD_PARTY,
45
- details: {
46
- threadId
90
+ const data = JSON.parse(text);
91
+ if (data && typeof data === "object" && "value" in data) {
92
+ if (typeof data.value === "string") {
93
+ try {
94
+ return JSON.parse(data.value);
95
+ } catch {
96
+ return data.value;
47
97
  }
48
- },
49
- error
50
- );
51
- this.logger?.trackException(mastraError);
52
- this.logger?.error(mastraError.toString());
98
+ }
99
+ return null;
100
+ }
101
+ return data;
102
+ } catch (error) {
103
+ const message = error instanceof Error ? error.message : String(error);
104
+ this.logger.error("Failed to parse text:", { message, text });
53
105
  return null;
54
106
  }
55
107
  }
56
- async listThreadsByResourceId(args) {
57
- try {
58
- const { resourceId, page = 0, perPage: perPageInput, orderBy } = args;
59
- const perPage = normalizePerPage(perPageInput, 100);
60
- if (page < 0) {
61
- throw new MastraError(
62
- {
63
- id: createStorageErrorId("CLOUDFLARE", "LIST_THREADS_BY_RESOURCE_ID", "INVALID_PAGE"),
64
- domain: ErrorDomain.STORAGE,
65
- category: ErrorCategory.USER,
66
- details: { page }
67
- },
68
- new Error("page must be >= 0")
69
- );
70
- }
71
- const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
72
- const { field, direction } = this.parseOrderBy(orderBy);
73
- const prefix = this.operations.namespacePrefix ? `${this.operations.namespacePrefix}:` : "";
74
- const keyObjs = await this.operations.listKV(TABLE_THREADS, { prefix: `${prefix}${TABLE_THREADS}` });
75
- const threads = [];
76
- for (const { name: key } of keyObjs) {
77
- const data = await this.operations.getKV(TABLE_THREADS, key);
78
- if (!data) continue;
79
- if (data.resourceId !== resourceId) continue;
80
- threads.push(data);
81
- }
82
- threads.sort((a, b) => {
83
- const aTime = new Date(a[field] || 0).getTime();
84
- const bTime = new Date(b[field] || 0).getTime();
85
- return direction === "ASC" ? aTime - bTime : bTime - aTime;
86
- });
87
- const end = perPageInput === false ? threads.length : offset + perPage;
88
- const paginatedThreads = threads.slice(offset, end);
108
+ async createNamespaceById(title) {
109
+ if (this.bindings) {
89
110
  return {
90
- page,
91
- perPage: perPageForResponse,
92
- total: threads.length,
93
- hasMore: perPageInput === false ? false : offset + perPage < threads.length,
94
- threads: paginatedThreads
111
+ id: title,
112
+ title,
113
+ supports_url_encoding: true
95
114
  };
96
- } catch (error) {
97
- throw new MastraError(
98
- {
99
- id: createStorageErrorId("CLOUDFLARE", "LIST_THREADS_BY_RESOURCE_ID", "FAILED"),
100
- domain: ErrorDomain.STORAGE,
101
- category: ErrorCategory.THIRD_PARTY,
102
- text: "Failed to get threads by resource ID with pagination"
103
- },
104
- error
105
- );
106
115
  }
116
+ return await this.client.kv.namespaces.create({
117
+ account_id: this.accountId,
118
+ title
119
+ });
107
120
  }
108
- async saveThread({ thread }) {
121
+ async createNamespace(namespaceName) {
109
122
  try {
110
- await this.operations.insert({ tableName: TABLE_THREADS, record: thread });
111
- return thread;
123
+ const response = await this.createNamespaceById(namespaceName);
124
+ return response.id;
112
125
  } catch (error) {
113
- throw new MastraError(
114
- {
115
- id: createStorageErrorId("CLOUDFLARE", "SAVE_THREAD", "FAILED"),
116
- domain: ErrorDomain.STORAGE,
117
- category: ErrorCategory.THIRD_PARTY,
118
- details: {
119
- threadId: thread.id
120
- }
121
- },
122
- error
123
- );
126
+ if (error.message && error.message.includes("already exists")) {
127
+ const namespaces = await this.listNamespaces();
128
+ const namespace = namespaces.result.find((ns) => ns.title === namespaceName);
129
+ if (namespace) return namespace.id;
130
+ }
131
+ this.logger.error("Error creating namespace:", error);
132
+ throw new Error(`Failed to create namespace ${namespaceName}: ${error.message}`);
124
133
  }
125
134
  }
126
- async updateThread({
127
- id,
128
- title,
129
- metadata
130
- }) {
131
- try {
132
- const thread = await this.getThreadById({ threadId: id });
133
- if (!thread) {
134
- throw new Error(`Thread ${id} not found`);
135
- }
136
- const updatedThread = {
137
- ...thread,
138
- title,
139
- metadata: this.ensureMetadata({
140
- ...thread.metadata ?? {},
141
- ...metadata
142
- }),
143
- updatedAt: /* @__PURE__ */ new Date()
135
+ async listNamespaces() {
136
+ if (this.bindings) {
137
+ return {
138
+ result: Object.keys(this.bindings).map((name) => ({
139
+ id: name,
140
+ title: name,
141
+ supports_url_encoding: true
142
+ }))
144
143
  };
145
- await this.operations.insert({ tableName: TABLE_THREADS, record: updatedThread });
146
- return updatedThread;
147
- } catch (error) {
148
- throw new MastraError(
149
- {
150
- id: createStorageErrorId("CLOUDFLARE", "UPDATE_THREAD", "FAILED"),
151
- domain: ErrorDomain.STORAGE,
152
- category: ErrorCategory.THIRD_PARTY,
153
- details: {
154
- threadId: id,
155
- title
156
- }
157
- },
158
- error
159
- );
160
144
  }
161
- }
162
- getMessageKey(threadId, messageId) {
163
- try {
164
- return this.operations.getKey(TABLE_MESSAGES, { threadId, id: messageId });
165
- } catch (error) {
166
- const message = error instanceof Error ? error.message : String(error);
167
- this.logger?.error(`Error getting message key for thread ${threadId} and message ${messageId}:`, { message });
168
- throw error;
145
+ let allNamespaces = [];
146
+ let currentPage = 1;
147
+ const perPage = 50;
148
+ let morePagesExist = true;
149
+ while (morePagesExist) {
150
+ const response = await this.client.kv.namespaces.list({
151
+ account_id: this.accountId,
152
+ page: currentPage,
153
+ per_page: perPage
154
+ });
155
+ if (response.result) {
156
+ allNamespaces = allNamespaces.concat(response.result);
157
+ }
158
+ morePagesExist = response.result ? response.result.length === perPage : false;
159
+ if (morePagesExist) {
160
+ currentPage++;
161
+ }
169
162
  }
163
+ return { result: allNamespaces };
170
164
  }
171
- getThreadMessagesKey(threadId) {
165
+ async getNamespaceIdByName(namespaceName) {
172
166
  try {
173
- return this.operations.getKey(TABLE_MESSAGES, { threadId, id: "messages" });
167
+ const response = await this.listNamespaces();
168
+ const namespace = response.result.find((ns) => ns.title === namespaceName);
169
+ return namespace ? namespace.id : null;
174
170
  } catch (error) {
175
- const message = error instanceof Error ? error.message : String(error);
176
- this.logger?.error(`Error getting thread messages key for thread ${threadId}:`, { message });
177
- throw error;
171
+ this.logger.error(`Failed to get namespace ID for ${namespaceName}:`, error);
172
+ return null;
178
173
  }
179
174
  }
180
- async deleteThread({ threadId }) {
175
+ async getOrCreateNamespaceId(namespaceName) {
176
+ let namespaceId = await this.getNamespaceIdByName(namespaceName);
177
+ if (!namespaceId) {
178
+ namespaceId = await this.createNamespace(namespaceName);
179
+ }
180
+ return namespaceId;
181
+ }
182
+ async getNamespaceId(tableName) {
183
+ const prefix = this.namespacePrefix ? `${this.namespacePrefix}_` : "";
181
184
  try {
182
- const thread = await this.getThreadById({ threadId });
183
- if (!thread) {
184
- throw new Error(`Thread ${threadId} not found`);
185
- }
186
- const messageKeys = await this.operations.listKV(TABLE_MESSAGES);
187
- const threadMessageKeys = messageKeys.filter((key) => key.name.includes(`${TABLE_MESSAGES}:${threadId}:`));
188
- await Promise.all([
189
- // Delete message order
190
- this.operations.deleteKV(TABLE_MESSAGES, this.getThreadMessagesKey(threadId)),
191
- // Delete all messages
192
- ...threadMessageKeys.map((key) => this.operations.deleteKV(TABLE_MESSAGES, key.name)),
193
- // Delete thread
194
- this.operations.deleteKV(TABLE_THREADS, this.operations.getKey(TABLE_THREADS, { id: threadId }))
195
- ]);
185
+ return await this.getOrCreateNamespaceId(`${prefix}${tableName}`);
196
186
  } catch (error) {
197
- throw new MastraError(
198
- {
199
- id: createStorageErrorId("CLOUDFLARE", "DELETE_THREAD", "FAILED"),
200
- domain: ErrorDomain.STORAGE,
201
- category: ErrorCategory.THIRD_PARTY,
202
- details: {
203
- threadId
204
- }
205
- },
206
- error
207
- );
187
+ this.logger.error("Error fetching namespace ID:", error);
188
+ throw new Error(`Failed to fetch namespace ID for table ${tableName}: ${error.message}`);
208
189
  }
209
190
  }
210
- /**
211
- * Searches all threads in the KV store to find a message by its ID.
212
- *
213
- * **Performance Warning**: This method sequentially scans all threads to locate
214
- * the message. For stores with many threads, this can result in significant
215
- * latency and API calls. When possible, callers should provide the `threadId`
216
- * directly to avoid this full scan.
217
- *
218
- * @param messageId - The globally unique message ID to search for
219
- * @returns The message with its threadId if found, null otherwise
220
- */
221
- async findMessageInAnyThread(messageId) {
191
+ async getNamespaceValue(tableName, key) {
222
192
  try {
223
- const prefix = this.operations.namespacePrefix ? `${this.operations.namespacePrefix}:` : "";
224
- const threadKeys = await this.operations.listKV(TABLE_THREADS, { prefix: `${prefix}${TABLE_THREADS}` });
225
- for (const { name: threadKey } of threadKeys) {
226
- const threadId = threadKey.split(":").pop();
227
- if (!threadId || threadId === "messages") continue;
228
- const messageKey = this.getMessageKey(threadId, messageId);
229
- const message = await this.operations.getKV(TABLE_MESSAGES, messageKey);
230
- if (message) {
231
- return { ...message, threadId };
232
- }
193
+ if (this.bindings) {
194
+ const binding = this.getBinding(tableName);
195
+ const result = await binding.getWithMetadata(key, "text");
196
+ if (!result) return null;
197
+ return JSON.stringify(result);
198
+ } else {
199
+ const namespaceId = await this.getNamespaceId(tableName);
200
+ const response = await this.client.kv.namespaces.values.get(namespaceId, key, {
201
+ account_id: this.accountId
202
+ });
203
+ return await response.text();
233
204
  }
234
- return null;
235
205
  } catch (error) {
236
- this.logger?.error(`Error finding message ${messageId} in any thread:`, error);
206
+ if (error.message && error.message.includes("key not found")) {
207
+ return null;
208
+ }
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ this.logger.error(`Failed to get value for ${tableName} ${key}:`, { message });
211
+ throw error;
212
+ }
213
+ }
214
+ async getKV(tableName, key) {
215
+ try {
216
+ const text = await this.getNamespaceValue(tableName, key);
217
+ return this.safeParse(text);
218
+ } catch (error) {
219
+ this.logger.error(`Failed to get KV value for ${tableName}:${key}:`, error);
220
+ throw new Error(`Failed to get KV value: ${error.message}`);
221
+ }
222
+ }
223
+ async getTableSchema(tableName) {
224
+ try {
225
+ const schemaKey = this.getSchemaKey(tableName);
226
+ return await this.getKV(tableName, schemaKey);
227
+ } catch (error) {
228
+ const message = error instanceof Error ? error.message : String(error);
229
+ this.logger.error(`Failed to get schema for ${tableName}:`, { message });
237
230
  return null;
238
231
  }
239
232
  }
240
- /**
241
- * Queue for serializing sorted order updates.
242
- * Updates the sorted order for a given key. This operation is eventually consistent.
243
- */
244
- updateQueue = /* @__PURE__ */ new Map();
245
- async updateSorting(threadMessages) {
246
- return threadMessages.map((msg) => ({
247
- message: msg,
248
- // Use _index if available, otherwise timestamp, matching Upstash
249
- score: msg._index !== void 0 ? msg._index : msg.createdAt.getTime()
250
- })).sort((a, b) => a.score - b.score).map((item) => ({
251
- id: item.message.id,
252
- score: item.score
253
- }));
233
+ validateColumnValue(value, column) {
234
+ if (value === void 0 || value === null) {
235
+ return column.nullable ?? false;
236
+ }
237
+ switch (column.type) {
238
+ case "text":
239
+ case "uuid":
240
+ return typeof value === "string";
241
+ case "integer":
242
+ case "bigint":
243
+ return typeof value === "number";
244
+ case "timestamp":
245
+ return value instanceof Date || typeof value === "string" && !isNaN(Date.parse(value));
246
+ case "jsonb":
247
+ if (typeof value !== "object") return false;
248
+ try {
249
+ JSON.stringify(value);
250
+ return true;
251
+ } catch {
252
+ return false;
253
+ }
254
+ default:
255
+ return false;
256
+ }
254
257
  }
255
- /**
256
- * Updates the sorted order for a given key. This operation is eventually consistent.
257
- * Note: Operations on the same orderKey are serialized using a queue to prevent
258
- * concurrent updates from conflicting with each other.
259
- */
260
- async updateSortedMessages(orderKey, newEntries) {
261
- const currentPromise = this.updateQueue.get(orderKey) || Promise.resolve();
262
- const nextPromise = currentPromise.then(async () => {
263
- try {
264
- const currentOrder = await this.getSortedMessages(orderKey);
265
- const orderMap = new Map(currentOrder.map((entry) => [entry.id, entry]));
266
- for (const entry of newEntries) {
267
- orderMap.set(entry.id, entry);
258
+ async validateAgainstSchema(record, schema) {
259
+ try {
260
+ if (!schema || typeof schema !== "object" || schema.value === null) {
261
+ throw new Error("Invalid schema format");
262
+ }
263
+ for (const [columnName, column] of Object.entries(schema)) {
264
+ const value = record[columnName];
265
+ if (column.primaryKey && (value === void 0 || value === null)) {
266
+ throw new Error(`Missing primary key value for column ${columnName}`);
268
267
  }
269
- const updatedOrder = Array.from(orderMap.values()).sort((a, b) => a.score - b.score);
270
- await this.operations.putKV({
271
- tableName: TABLE_MESSAGES,
272
- key: orderKey,
273
- value: JSON.stringify(updatedOrder)
274
- });
275
- } catch (error) {
276
- const message = error instanceof Error ? error.message : String(error);
277
- this.logger?.error(`Error updating sorted order for key ${orderKey}:`, { message });
278
- throw error;
279
- } finally {
280
- if (this.updateQueue.get(orderKey) === nextPromise) {
281
- this.updateQueue.delete(orderKey);
268
+ if (!this.validateColumnValue(value, column)) {
269
+ const valueType = value === null ? "null" : typeof value;
270
+ throw new Error(`Invalid value for column ${columnName}: expected ${column.type}, got ${valueType}`);
282
271
  }
283
272
  }
284
- });
285
- this.updateQueue.set(orderKey, nextPromise);
286
- return nextPromise;
287
- }
288
- async getSortedMessages(orderKey) {
289
- const raw = await this.operations.getKV(TABLE_MESSAGES, orderKey);
290
- if (!raw) return [];
291
- try {
292
- const arr = JSON.parse(typeof raw === "string" ? raw : JSON.stringify(raw));
293
- return Array.isArray(arr) ? arr : [];
294
- } catch (e) {
295
- this.logger?.error(`Error parsing order data for key ${orderKey}:`, { e });
296
- return [];
273
+ } catch (error) {
274
+ const message = error instanceof Error ? error.message : String(error);
275
+ this.logger.error(`Error validating record against schema:`, { message, record, schema });
276
+ throw error;
297
277
  }
298
278
  }
299
- async migrateMessage(messageId, fromThreadId, toThreadId) {
279
+ async validateRecord(record, tableName) {
300
280
  try {
301
- const oldMessageKey = this.getMessageKey(fromThreadId, messageId);
302
- const message = await this.operations.getKV(TABLE_MESSAGES, oldMessageKey);
303
- if (!message) return;
304
- const updatedMessage = {
305
- ...message,
306
- threadId: toThreadId
307
- };
308
- const newMessageKey = this.getMessageKey(toThreadId, messageId);
309
- await this.operations.putKV({ tableName: TABLE_MESSAGES, key: newMessageKey, value: updatedMessage });
310
- const oldOrderKey = this.getThreadMessagesKey(fromThreadId);
311
- const oldEntries = await this.getSortedMessages(oldOrderKey);
312
- const filteredEntries = oldEntries.filter((entry) => entry.id !== messageId);
313
- await this.updateSortedMessages(oldOrderKey, filteredEntries);
314
- const newOrderKey = this.getThreadMessagesKey(toThreadId);
315
- const newEntries = await this.getSortedMessages(newOrderKey);
316
- const newEntry = { id: messageId, score: Date.now() };
317
- newEntries.push(newEntry);
318
- await this.updateSortedMessages(newOrderKey, newEntries);
319
- await this.operations.deleteKV(TABLE_MESSAGES, oldMessageKey);
281
+ if (!record || typeof record !== "object") {
282
+ throw new Error("Record must be an object");
283
+ }
284
+ const recordTyped = record;
285
+ const schema = await this.getTableSchema(tableName);
286
+ if (schema) {
287
+ await this.validateAgainstSchema(recordTyped, schema);
288
+ return;
289
+ }
290
+ switch (tableName) {
291
+ case TABLE_THREADS:
292
+ if (!("id" in recordTyped) || !("resourceId" in recordTyped) || !("title" in recordTyped)) {
293
+ throw new Error("Thread record missing required fields");
294
+ }
295
+ break;
296
+ case TABLE_MESSAGES:
297
+ if (!("id" in recordTyped) || !("threadId" in recordTyped) || !("content" in recordTyped) || !("role" in recordTyped)) {
298
+ throw new Error("Message record missing required fields");
299
+ }
300
+ break;
301
+ case TABLE_WORKFLOW_SNAPSHOT:
302
+ if (!("workflow_name" in recordTyped) || !("run_id" in recordTyped)) {
303
+ throw new Error("Workflow record missing required fields");
304
+ }
305
+ break;
306
+ case TABLE_TRACES:
307
+ if (!("id" in recordTyped)) {
308
+ throw new Error("Trace record missing required fields");
309
+ }
310
+ break;
311
+ case TABLE_SCORERS:
312
+ if (!("id" in recordTyped) || !("scorerId" in recordTyped)) {
313
+ throw new Error("Score record missing required fields");
314
+ }
315
+ break;
316
+ default:
317
+ throw new Error(`Unknown table type: ${tableName}`);
318
+ }
320
319
  } catch (error) {
321
- this.logger?.error(`Error migrating message ${messageId} from ${fromThreadId} to ${toThreadId}:`, error);
320
+ const message = error instanceof Error ? error.message : String(error);
321
+ this.logger.error(`Failed to validate record for ${tableName}:`, { message, record });
322
322
  throw error;
323
323
  }
324
324
  }
325
- async saveMessages(args) {
326
- const { messages } = args;
327
- if (!Array.isArray(messages) || messages.length === 0) return { messages: [] };
325
+ async insert({ tableName, record }) {
328
326
  try {
329
- const validatedMessages = messages.map((message, index) => {
330
- const errors = [];
331
- if (!message.id) errors.push("id is required");
332
- if (!message.threadId) errors.push("threadId is required");
333
- if (!message.content) errors.push("content is required");
334
- if (!message.role) errors.push("role is required");
335
- if (!message.createdAt) errors.push("createdAt is required");
336
- if (message.resourceId === null || message.resourceId === void 0) errors.push("resourceId is required");
337
- if (errors.length > 0) {
338
- throw new Error(`Invalid message at index ${index}: ${errors.join(", ")}`);
339
- }
340
- return {
341
- ...message,
342
- createdAt: ensureDate(message.createdAt),
343
- type: message.type || "v2",
344
- _index: index
345
- };
346
- }).filter((m) => !!m);
347
- const messageMigrationTasks = [];
348
- for (const message of validatedMessages) {
349
- const existingMessage = await this.findMessageInAnyThread(message.id);
350
- this.logger?.debug(
351
- `Checking message ${message.id}: existing=${existingMessage?.threadId}, new=${message.threadId}`
352
- );
353
- if (existingMessage && existingMessage.threadId && existingMessage.threadId !== message.threadId) {
354
- this.logger?.debug(`Migrating message ${message.id} from ${existingMessage.threadId} to ${message.threadId}`);
355
- messageMigrationTasks.push(this.migrateMessage(message.id, existingMessage.threadId, message.threadId));
356
- }
357
- }
358
- await Promise.all(messageMigrationTasks);
359
- const messagesByThread = validatedMessages.reduce((acc, message) => {
360
- if (message.threadId && !acc.has(message.threadId)) {
361
- acc.set(message.threadId, []);
362
- }
363
- if (message.threadId) {
364
- acc.get(message.threadId).push(message);
365
- }
366
- return acc;
367
- }, /* @__PURE__ */ new Map());
368
- await Promise.all(
369
- Array.from(messagesByThread.entries()).map(async ([threadId, threadMessages]) => {
370
- try {
371
- const thread = await this.getThreadById({ threadId });
372
- if (!thread) {
373
- throw new Error(`Thread ${threadId} not found`);
374
- }
375
- await Promise.all(
376
- threadMessages.map(async (message) => {
377
- const key = this.getMessageKey(threadId, message.id);
378
- const { _index, ...cleanMessage } = message;
379
- const serializedMessage = {
380
- ...cleanMessage,
381
- createdAt: serializeDate(cleanMessage.createdAt)
382
- };
383
- this.logger?.debug(`Saving message ${message.id}`, {
384
- contentSummary: this.summarizeMessageContent(serializedMessage.content)
385
- });
386
- await this.operations.putKV({ tableName: TABLE_MESSAGES, key, value: serializedMessage });
387
- })
388
- );
389
- const orderKey = this.getThreadMessagesKey(threadId);
390
- const entries = await this.updateSorting(threadMessages);
391
- await this.updateSortedMessages(orderKey, entries);
392
- const updatedThread = {
393
- ...thread,
394
- updatedAt: /* @__PURE__ */ new Date()
395
- };
396
- await this.operations.putKV({
397
- tableName: TABLE_THREADS,
398
- key: this.operations.getKey(TABLE_THREADS, { id: threadId }),
399
- value: updatedThread
400
- });
401
- } catch (error) {
402
- throw new MastraError(
403
- {
404
- id: createStorageErrorId("CLOUDFLARE", "SAVE_MESSAGES", "FAILED"),
405
- domain: ErrorDomain.STORAGE,
406
- category: ErrorCategory.THIRD_PARTY,
407
- details: {
408
- threadId
409
- }
410
- },
411
- error
412
- );
413
- }
414
- })
415
- );
416
- const prepared = validatedMessages.map(
417
- ({ _index, ...message }) => ({ ...message, type: message.type !== "v2" ? message.type : void 0 })
418
- );
419
- const list = new MessageList().add(prepared, "memory");
420
- return { messages: list.get.all.db() };
327
+ const key = this.getKey(tableName, record);
328
+ const processedRecord = { ...record };
329
+ await this.validateRecord(processedRecord, tableName);
330
+ await this.putKV({ tableName, key, value: processedRecord });
421
331
  } catch (error) {
422
332
  throw new MastraError(
423
333
  {
424
- id: createStorageErrorId("CLOUDFLARE", "SAVE_MESSAGES", "FAILED"),
334
+ id: createStorageErrorId("CLOUDFLARE", "INSERT", "FAILED"),
425
335
  domain: ErrorDomain.STORAGE,
426
- category: ErrorCategory.THIRD_PARTY
336
+ category: ErrorCategory.THIRD_PARTY,
337
+ details: {
338
+ tableName
339
+ }
427
340
  },
428
341
  error
429
342
  );
430
343
  }
431
344
  }
432
- async getRank(orderKey, id) {
433
- const order = await this.getSortedMessages(orderKey);
434
- const index = order.findIndex((item) => item.id === id);
435
- return index >= 0 ? index : null;
436
- }
437
- async getRange(orderKey, start, end) {
438
- const order = await this.getSortedMessages(orderKey);
439
- const actualStart = start < 0 ? Math.max(0, order.length + start) : start;
440
- const actualEnd = end < 0 ? order.length + end : Math.min(end, order.length - 1);
441
- const sliced = order.slice(actualStart, actualEnd + 1);
442
- return sliced.map((item) => item.id);
443
- }
444
- async getLastN(orderKey, n) {
445
- return this.getRange(orderKey, -n, -1);
446
- }
447
- async getFullOrder(orderKey) {
448
- return this.getRange(orderKey, 0, -1);
449
- }
450
- /**
451
- * Retrieves messages specified in the include array along with their surrounding context.
452
- *
453
- * **Performance Note**: When `threadId` is not provided in an include entry, this method
454
- * must call `findMessageInAnyThread` which sequentially scans all threads in the KV store.
455
- * For optimal performance, callers should provide `threadId` in include entries when known.
456
- *
457
- * @param include - Array of message IDs to include, optionally with context windows
458
- * @param messageIds - Set to accumulate the message IDs that should be fetched
459
- */
460
- async getIncludedMessagesWithContext(include, messageIds) {
461
- await Promise.all(
462
- include.map(async (item) => {
463
- let targetThreadId = item.threadId;
464
- if (!targetThreadId) {
465
- const foundMessage = await this.findMessageInAnyThread(item.id);
466
- if (!foundMessage) return;
467
- targetThreadId = foundMessage.threadId;
468
- }
469
- if (!targetThreadId) return;
470
- const threadMessagesKey = this.getThreadMessagesKey(targetThreadId);
471
- messageIds.add(item.id);
472
- if (!item.withPreviousMessages && !item.withNextMessages) return;
473
- const rank = await this.getRank(threadMessagesKey, item.id);
474
- if (rank === null) return;
475
- if (item.withPreviousMessages) {
476
- const prevIds = await this.getRange(
477
- threadMessagesKey,
478
- Math.max(0, rank - item.withPreviousMessages),
479
- rank - 1
480
- );
481
- prevIds.forEach((id) => messageIds.add(id));
482
- }
483
- if (item.withNextMessages) {
484
- const nextIds = await this.getRange(threadMessagesKey, rank + 1, rank + item.withNextMessages);
485
- nextIds.forEach((id) => messageIds.add(id));
486
- }
487
- })
488
- );
489
- }
490
- async getRecentMessages(threadId, limit, messageIds) {
491
- if (!threadId.trim()) throw new Error("threadId must be a non-empty string");
492
- if (limit <= 0) return;
493
- try {
494
- const threadMessagesKey = this.getThreadMessagesKey(threadId);
495
- const latestIds = await this.getLastN(threadMessagesKey, limit);
496
- latestIds.forEach((id) => messageIds.add(id));
497
- } catch {
498
- this.logger?.debug(`No message order found for thread ${threadId}, skipping latest messages`);
499
- }
500
- }
501
- /**
502
- * Fetches and parses messages from one or more threads.
503
- *
504
- * **Performance Note**: When neither `include` entries with `threadId` nor `targetThreadId`
505
- * are provided, this method falls back to `findMessageInAnyThread` which scans all threads.
506
- * For optimal performance, provide `threadId` in include entries or specify `targetThreadId`.
507
- */
508
- async fetchAndParseMessagesFromMultipleThreads(messageIds, include, targetThreadId) {
509
- const messageIdToThreadId = /* @__PURE__ */ new Map();
510
- if (include) {
511
- for (const item of include) {
512
- if (item.threadId) {
513
- messageIdToThreadId.set(item.id, item.threadId);
514
- }
515
- }
516
- }
517
- const messages = await Promise.all(
518
- messageIds.map(async (id) => {
519
- try {
520
- let threadId = messageIdToThreadId.get(id);
521
- if (!threadId) {
522
- if (targetThreadId) {
523
- threadId = targetThreadId;
524
- } else {
525
- const foundMessage = await this.findMessageInAnyThread(id);
526
- if (foundMessage) {
527
- threadId = foundMessage.threadId;
528
- }
529
- }
530
- }
531
- if (!threadId) return null;
532
- const key = this.getMessageKey(threadId, id);
533
- const data = await this.operations.getKV(TABLE_MESSAGES, key);
534
- if (!data) return null;
535
- const parsed = typeof data === "string" ? JSON.parse(data) : data;
536
- this.logger?.debug(`Retrieved message ${id} from thread ${threadId}`, {
537
- contentSummary: this.summarizeMessageContent(parsed.content)
538
- });
539
- return parsed;
540
- } catch (error) {
541
- const message = error instanceof Error ? error.message : String(error);
542
- this.logger?.error(`Error retrieving message ${id}:`, { message });
543
- return null;
544
- }
545
- })
546
- );
547
- return messages.filter((msg) => msg !== null);
548
- }
549
- /**
550
- * Retrieves messages by their IDs.
551
- *
552
- * **Performance Warning**: This method calls `findMessageInAnyThread` for each message ID,
553
- * which scans all threads in the KV store. For large numbers of messages or threads,
554
- * this can result in significant latency. Consider using `listMessages` with specific
555
- * thread IDs when the thread context is known.
556
- */
557
- async listMessagesById({ messageIds }) {
558
- if (messageIds.length === 0) return { messages: [] };
345
+ async load({ tableName, keys }) {
559
346
  try {
560
- const messages = (await Promise.all(messageIds.map((id) => this.findMessageInAnyThread(id)))).filter(
561
- (result) => !!result
562
- );
563
- const prepared = messages.map(({ _index, ...message }) => ({
564
- ...message,
565
- ...message.type !== `v2` && { type: message.type },
566
- createdAt: ensureDate(message.createdAt)
567
- }));
568
- const list = new MessageList().add(prepared, "memory");
569
- return { messages: list.get.all.db() };
347
+ const key = this.getKey(tableName, keys);
348
+ const data = await this.getKV(tableName, key);
349
+ if (!data) return null;
350
+ return data;
570
351
  } catch (error) {
571
352
  const mastraError = new MastraError(
572
353
  {
573
- id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES_BY_ID", "FAILED"),
354
+ id: createStorageErrorId("CLOUDFLARE", "LOAD", "FAILED"),
574
355
  domain: ErrorDomain.STORAGE,
575
356
  category: ErrorCategory.THIRD_PARTY,
576
- text: `Error retrieving messages by ID`,
577
357
  details: {
578
- messageIds: JSON.stringify(messageIds)
358
+ tableName
579
359
  }
580
360
  },
581
361
  error
582
362
  );
583
363
  this.logger?.trackException(mastraError);
584
364
  this.logger?.error(mastraError.toString());
585
- return { messages: [] };
365
+ return null;
586
366
  }
587
367
  }
588
- async listMessages(args) {
589
- const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
590
- const threadIds = Array.isArray(threadId) ? threadId : [threadId];
591
- const isValidThreadId = (id) => typeof id === "string" && id.trim().length > 0;
592
- if (threadIds.length === 0 || threadIds.some((id) => !isValidThreadId(id))) {
368
+ async batchInsert(input) {
369
+ if (!input.records || input.records.length === 0) return;
370
+ try {
371
+ await Promise.all(
372
+ input.records.map(async (record) => {
373
+ const key = this.getKey(input.tableName, record);
374
+ await this.putKV({ tableName: input.tableName, key, value: record });
375
+ })
376
+ );
377
+ } catch (error) {
593
378
  throw new MastraError(
594
379
  {
595
- id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES", "INVALID_THREAD_ID"),
380
+ id: createStorageErrorId("CLOUDFLARE", "BATCH_INSERT", "FAILED"),
596
381
  domain: ErrorDomain.STORAGE,
597
382
  category: ErrorCategory.THIRD_PARTY,
598
- details: { threadId: Array.isArray(threadId) ? JSON.stringify(threadId) : String(threadId) }
599
- },
600
- new Error("threadId must be a non-empty string or array of non-empty strings")
383
+ text: `Error in batch insert for table ${input.tableName}`,
384
+ details: {
385
+ tableName: input.tableName
386
+ }
387
+ },
388
+ error
601
389
  );
602
390
  }
603
- const perPage = normalizePerPage(perPageInput, 40);
604
- const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
391
+ }
392
+ safeSerialize(data) {
393
+ return typeof data === "string" ? data : JSON.stringify(data);
394
+ }
395
+ async putNamespaceValue({
396
+ tableName,
397
+ key,
398
+ value,
399
+ metadata
400
+ }) {
605
401
  try {
606
- if (page < 0) {
607
- throw new MastraError(
608
- {
609
- id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES", "INVALID_PAGE"),
610
- domain: ErrorDomain.STORAGE,
611
- category: ErrorCategory.USER,
612
- details: { page }
613
- },
614
- new Error("page must be >= 0")
615
- );
616
- }
617
- const { field, direction } = this.parseOrderBy(orderBy, "ASC");
618
- const threadMessageIds = /* @__PURE__ */ new Set();
619
- for (const tid of threadIds) {
620
- try {
621
- const threadMessagesKey = this.getThreadMessagesKey(tid);
622
- const allIds = await this.getFullOrder(threadMessagesKey);
623
- allIds.forEach((id) => threadMessageIds.add(id));
624
- } catch {
625
- }
626
- }
627
- const threadMessages = await this.fetchAndParseMessagesFromMultipleThreads(
628
- Array.from(threadMessageIds),
629
- void 0,
630
- threadIds.length === 1 ? threadIds[0] : void 0
631
- );
632
- let filteredThreadMessages = threadMessages;
633
- if (resourceId) {
634
- filteredThreadMessages = filteredThreadMessages.filter((msg) => msg.resourceId === resourceId);
635
- }
636
- const dateRange = filter?.dateRange;
637
- if (dateRange) {
638
- filteredThreadMessages = filteredThreadMessages.filter((msg) => {
639
- const messageDate = new Date(msg.createdAt);
640
- if (dateRange.start && messageDate < new Date(dateRange.start)) return false;
641
- if (dateRange.end && messageDate > new Date(dateRange.end)) return false;
642
- return true;
643
- });
644
- }
645
- const total = filteredThreadMessages.length;
646
- if (perPage === 0 && (!include || include.length === 0)) {
647
- return {
648
- messages: [],
649
- total,
650
- page,
651
- perPage: perPageForResponse,
652
- hasMore: offset < total
653
- };
654
- }
655
- filteredThreadMessages.sort((a, b) => {
656
- const timeA = new Date(a.createdAt).getTime();
657
- const timeB = new Date(b.createdAt).getTime();
658
- const timeDiff = direction === "ASC" ? timeA - timeB : timeB - timeA;
659
- if (timeDiff === 0) {
660
- return a.id.localeCompare(b.id);
661
- }
662
- return timeDiff;
663
- });
664
- let paginatedMessages;
665
- if (perPage === 0) {
666
- paginatedMessages = [];
667
- } else if (perPage === Number.MAX_SAFE_INTEGER) {
668
- paginatedMessages = filteredThreadMessages;
402
+ const serializedValue = this.safeSerialize(value);
403
+ const serializedMetadata = metadata ? this.safeSerialize(metadata) : "";
404
+ if (this.bindings) {
405
+ const binding = this.getBinding(tableName);
406
+ await binding.put(key, serializedValue, { metadata: serializedMetadata });
669
407
  } else {
670
- paginatedMessages = filteredThreadMessages.slice(offset, offset + perPage);
671
- }
672
- let includedMessages = [];
673
- if (include && include.length > 0) {
674
- const includedMessageIds = /* @__PURE__ */ new Set();
675
- await this.getIncludedMessagesWithContext(include, includedMessageIds);
676
- const paginatedIds = new Set(paginatedMessages.map((m) => m.id));
677
- const idsToFetch = Array.from(includedMessageIds).filter((id) => !paginatedIds.has(id));
678
- if (idsToFetch.length > 0) {
679
- includedMessages = await this.fetchAndParseMessagesFromMultipleThreads(idsToFetch, include, void 0);
680
- }
681
- }
682
- const seenIds = /* @__PURE__ */ new Set();
683
- const allMessages = [];
684
- for (const msg of paginatedMessages) {
685
- if (!seenIds.has(msg.id)) {
686
- allMessages.push(msg);
687
- seenIds.add(msg.id);
688
- }
689
- }
690
- for (const msg of includedMessages) {
691
- if (!seenIds.has(msg.id)) {
692
- allMessages.push(msg);
693
- seenIds.add(msg.id);
694
- }
695
- }
696
- allMessages.sort((a, b) => {
697
- const timeA = new Date(a.createdAt).getTime();
698
- const timeB = new Date(b.createdAt).getTime();
699
- const timeDiff = direction === "ASC" ? timeA - timeB : timeB - timeA;
700
- if (timeDiff === 0) {
701
- return a.id.localeCompare(b.id);
702
- }
703
- return timeDiff;
704
- });
705
- let filteredMessages = allMessages;
706
- const paginatedCount = paginatedMessages.length;
707
- if (total === 0 && filteredMessages.length === 0 && (!include || include.length === 0)) {
708
- return {
709
- messages: [],
710
- total: 0,
711
- page,
712
- perPage: perPageForResponse,
713
- hasMore: false
714
- };
408
+ const namespaceId = await this.getNamespaceId(tableName);
409
+ await this.client.kv.namespaces.values.update(namespaceId, key, {
410
+ account_id: this.accountId,
411
+ value: serializedValue,
412
+ metadata: serializedMetadata
413
+ });
715
414
  }
716
- const prepared = filteredMessages.map(({ _index, ...message }) => ({
717
- ...message,
718
- type: message.type !== "v2" ? message.type : void 0,
719
- createdAt: ensureDate(message.createdAt)
720
- }));
721
- const primaryThreadId = Array.isArray(threadId) ? threadId[0] : threadId;
722
- const list = new MessageList({ threadId: primaryThreadId, resourceId }).add(
723
- prepared,
724
- "memory"
725
- );
726
- let finalMessages = list.get.all.db();
727
- finalMessages = finalMessages.sort((a, b) => {
728
- const isDateField = field === "createdAt" || field === "updatedAt";
729
- const aVal = isDateField ? new Date(a[field]).getTime() : a[field];
730
- const bVal = isDateField ? new Date(b[field]).getTime() : b[field];
731
- if (aVal == null && bVal == null) return a.id.localeCompare(b.id);
732
- if (aVal == null) return 1;
733
- if (bVal == null) return -1;
734
- if (typeof aVal === "number" && typeof bVal === "number") {
735
- const cmp2 = direction === "ASC" ? aVal - bVal : bVal - aVal;
736
- return cmp2 !== 0 ? cmp2 : a.id.localeCompare(b.id);
737
- }
738
- const cmp = direction === "ASC" ? String(aVal).localeCompare(String(bVal)) : String(bVal).localeCompare(String(aVal));
739
- return cmp !== 0 ? cmp : a.id.localeCompare(b.id);
740
- });
741
- const threadIdSet = new Set(threadIds);
742
- const returnedThreadMessageIds = new Set(
743
- finalMessages.filter((m) => m.threadId && threadIdSet.has(m.threadId)).map((m) => m.id)
744
- );
745
- const allThreadMessagesReturned = returnedThreadMessageIds.size >= total;
746
- const hasMore = perPageInput !== false && !allThreadMessagesReturned && offset + paginatedCount < total;
747
- return {
748
- messages: finalMessages,
749
- total,
750
- page,
751
- perPage: perPageForResponse,
752
- hasMore
415
+ } catch (error) {
416
+ const message = error instanceof Error ? error.message : String(error);
417
+ this.logger.error(`Failed to put value for ${tableName} ${key}:`, { message });
418
+ throw error;
419
+ }
420
+ }
421
+ async putKV({
422
+ tableName,
423
+ key,
424
+ value,
425
+ metadata
426
+ }) {
427
+ try {
428
+ await this.putNamespaceValue({ tableName, key, value, metadata });
429
+ } catch (error) {
430
+ this.logger.error(`Failed to put KV value for ${tableName}:${key}:`, error);
431
+ throw new Error(`Failed to put KV value: ${error.message}`);
432
+ }
433
+ }
434
+ async createTable({
435
+ tableName,
436
+ schema
437
+ }) {
438
+ try {
439
+ const schemaKey = this.getSchemaKey(tableName);
440
+ const metadata = {
441
+ type: "table_schema",
442
+ tableName,
443
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
753
444
  };
445
+ await this.putKV({ tableName, key: schemaKey, value: schema, metadata });
754
446
  } catch (error) {
755
- const mastraError = new MastraError(
447
+ throw new MastraError(
756
448
  {
757
- id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES", "FAILED"),
449
+ id: createStorageErrorId("CLOUDFLARE", "CREATE_TABLE", "FAILED"),
758
450
  domain: ErrorDomain.STORAGE,
759
451
  category: ErrorCategory.THIRD_PARTY,
760
- text: `Failed to list messages for thread ${Array.isArray(threadId) ? threadId.join(",") : threadId}: ${error instanceof Error ? error.message : String(error)}`,
761
452
  details: {
762
- threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
763
- resourceId: resourceId ?? ""
453
+ tableName
764
454
  }
765
455
  },
766
456
  error
767
457
  );
768
- this.logger?.error?.(mastraError.toString());
769
- this.logger?.trackException?.(mastraError);
770
- return {
771
- messages: [],
772
- total: 0,
773
- page,
774
- perPage: perPageForResponse,
775
- hasMore: false
776
- };
777
458
  }
778
459
  }
779
- async updateMessages(args) {
460
+ async clearTable({ tableName }) {
780
461
  try {
781
- const { messages } = args;
782
- const updatedMessages = [];
783
- for (const messageUpdate of messages) {
784
- const { id, content, ...otherFields } = messageUpdate;
785
- const prefix = this.operations.namespacePrefix ? `${this.operations.namespacePrefix}:` : "";
786
- const keyObjs = await this.operations.listKV(TABLE_MESSAGES, { prefix: `${prefix}${TABLE_MESSAGES}` });
787
- let existingMessage = null;
788
- let messageKey = "";
789
- for (const { name: key } of keyObjs) {
790
- const data = await this.operations.getKV(TABLE_MESSAGES, key);
791
- if (data && data.id === id) {
792
- existingMessage = data;
793
- messageKey = key;
794
- break;
462
+ const keys = await this.listKV(tableName);
463
+ if (keys.length > 0) {
464
+ await Promise.all(keys.map((keyObj) => this.deleteKV(tableName, keyObj.name)));
465
+ }
466
+ } catch (error) {
467
+ throw new MastraError(
468
+ {
469
+ id: createStorageErrorId("CLOUDFLARE", "CLEAR_TABLE", "FAILED"),
470
+ domain: ErrorDomain.STORAGE,
471
+ category: ErrorCategory.THIRD_PARTY,
472
+ details: {
473
+ tableName
795
474
  }
796
- }
797
- if (!existingMessage) {
798
- continue;
799
- }
800
- const updatedMessage = {
801
- ...existingMessage,
802
- ...otherFields,
803
- id
804
- };
805
- if (content) {
806
- if (content.metadata !== void 0) {
807
- updatedMessage.content = {
808
- ...updatedMessage.content,
809
- metadata: {
810
- ...updatedMessage.content?.metadata,
811
- ...content.metadata
812
- }
813
- };
814
- }
815
- if (content.content !== void 0) {
816
- updatedMessage.content = {
817
- ...updatedMessage.content,
818
- content: content.content
819
- };
820
- }
821
- }
822
- if ("threadId" in messageUpdate && messageUpdate.threadId && messageUpdate.threadId !== existingMessage.threadId) {
823
- await this.operations.deleteKV(TABLE_MESSAGES, messageKey);
824
- updatedMessage.threadId = messageUpdate.threadId;
825
- const newMessageKey = this.getMessageKey(messageUpdate.threadId, id);
826
- await this.operations.putKV({
827
- tableName: TABLE_MESSAGES,
828
- key: newMessageKey,
829
- value: updatedMessage
830
- });
831
- if (existingMessage.threadId) {
832
- const sourceOrderKey = this.getThreadMessagesKey(existingMessage.threadId);
833
- const sourceEntries = await this.getSortedMessages(sourceOrderKey);
834
- const filteredEntries = sourceEntries.filter((entry) => entry.id !== id);
835
- await this.updateSortedMessages(sourceOrderKey, filteredEntries);
836
- }
837
- const destOrderKey = this.getThreadMessagesKey(messageUpdate.threadId);
838
- const destEntries = await this.getSortedMessages(destOrderKey);
839
- const newEntry = { id, score: Date.now() };
840
- destEntries.push(newEntry);
841
- await this.updateSortedMessages(destOrderKey, destEntries);
842
- } else {
843
- await this.operations.putKV({
844
- tableName: TABLE_MESSAGES,
845
- key: messageKey,
846
- value: updatedMessage
847
- });
848
- }
849
- const threadsToUpdate = /* @__PURE__ */ new Set();
850
- if (updatedMessage.threadId) {
851
- threadsToUpdate.add(updatedMessage.threadId);
852
- }
853
- if ("threadId" in messageUpdate && messageUpdate.threadId && messageUpdate.threadId !== existingMessage.threadId) {
854
- if (existingMessage.threadId) {
855
- threadsToUpdate.add(existingMessage.threadId);
856
- }
857
- threadsToUpdate.add(messageUpdate.threadId);
858
- }
859
- for (const threadId of threadsToUpdate) {
860
- const thread = await this.getThreadById({ threadId });
861
- if (thread) {
862
- const updatedThread = {
863
- ...thread,
864
- updatedAt: /* @__PURE__ */ new Date()
865
- };
866
- await this.operations.putKV({
867
- tableName: TABLE_THREADS,
868
- key: this.operations.getKey(TABLE_THREADS, { id: threadId }),
869
- value: updatedThread
870
- });
871
- }
872
- }
873
- updatedMessages.push(updatedMessage);
874
- }
875
- return updatedMessages;
876
- } catch (error) {
877
- throw new MastraError(
878
- {
879
- id: createStorageErrorId("CLOUDFLARE", "UPDATE_MESSAGES", "FAILED"),
880
- domain: ErrorDomain.STORAGE,
881
- category: ErrorCategory.THIRD_PARTY,
882
- text: "Failed to update messages"
883
475
  },
884
476
  error
885
477
  );
886
478
  }
887
479
  }
888
- async getResourceById({ resourceId }) {
480
+ async dropTable({ tableName }) {
889
481
  try {
890
- const data = await this.operations.getKV(TABLE_RESOURCES, resourceId);
891
- if (!data) return null;
892
- const resource = typeof data === "string" ? JSON.parse(data) : data;
893
- return {
894
- ...resource,
895
- createdAt: ensureDate(resource.createdAt),
896
- updatedAt: ensureDate(resource.updatedAt),
897
- metadata: this.ensureMetadata(resource.metadata)
898
- };
482
+ const keys = await this.listKV(tableName);
483
+ if (keys.length > 0) {
484
+ await Promise.all(keys.map((keyObj) => this.deleteKV(tableName, keyObj.name)));
485
+ }
899
486
  } catch (error) {
900
- const mastraError = new MastraError(
487
+ throw new MastraError(
901
488
  {
902
- id: createStorageErrorId("CLOUDFLARE", "GET_RESOURCE_BY_ID", "FAILED"),
489
+ id: createStorageErrorId("CLOUDFLARE", "DROP_TABLE", "FAILED"),
903
490
  domain: ErrorDomain.STORAGE,
904
491
  category: ErrorCategory.THIRD_PARTY,
905
492
  details: {
906
- resourceId
493
+ tableName
907
494
  }
908
495
  },
909
496
  error
910
497
  );
911
- this.logger?.trackException(mastraError);
912
- this.logger?.error(mastraError.toString());
913
- return null;
914
498
  }
915
499
  }
916
- async saveResource({ resource }) {
500
+ async listNamespaceKeys(tableName, options) {
917
501
  try {
918
- const resourceToSave = {
919
- ...resource,
920
- metadata: resource.metadata ? JSON.stringify(resource.metadata) : null
921
- };
922
- await this.operations.putKV({
923
- tableName: TABLE_RESOURCES,
924
- key: resource.id,
925
- value: resourceToSave
926
- });
927
- return resource;
502
+ if (this.bindings) {
503
+ const binding = this.getBinding(tableName);
504
+ const response = await binding.list({
505
+ limit: options?.limit || 1e3,
506
+ prefix: options?.prefix
507
+ });
508
+ return response.keys;
509
+ } else {
510
+ const namespaceId = await this.getNamespaceId(tableName);
511
+ const response = await this.client.kv.namespaces.keys.list(namespaceId, {
512
+ account_id: this.accountId,
513
+ limit: options?.limit || 1e3,
514
+ prefix: options?.prefix
515
+ });
516
+ return response.result;
517
+ }
928
518
  } catch (error) {
929
519
  throw new MastraError(
930
520
  {
931
- id: createStorageErrorId("CLOUDFLARE", "SAVE_RESOURCE", "FAILED"),
521
+ id: createStorageErrorId("CLOUDFLARE", "LIST_NAMESPACE_KEYS", "FAILED"),
932
522
  domain: ErrorDomain.STORAGE,
933
523
  category: ErrorCategory.THIRD_PARTY,
934
524
  details: {
935
- resourceId: resource.id
525
+ tableName
936
526
  }
937
527
  },
938
528
  error
939
529
  );
940
530
  }
941
531
  }
942
- async updateResource({
943
- resourceId,
944
- workingMemory,
945
- metadata
946
- }) {
947
- const existingResource = await this.getResourceById({ resourceId });
948
- if (!existingResource) {
949
- const newResource = {
950
- id: resourceId,
951
- workingMemory,
952
- metadata: metadata || {},
953
- createdAt: /* @__PURE__ */ new Date(),
954
- updatedAt: /* @__PURE__ */ new Date()
955
- };
956
- return this.saveResource({ resource: newResource });
532
+ async deleteNamespaceValue(tableName, key) {
533
+ if (this.bindings) {
534
+ const binding = this.getBinding(tableName);
535
+ await binding.delete(key);
536
+ } else {
537
+ const namespaceId = await this.getNamespaceId(tableName);
538
+ await this.client.kv.namespaces.values.delete(namespaceId, key, {
539
+ account_id: this.accountId
540
+ });
541
+ }
542
+ }
543
+ async deleteKV(tableName, key) {
544
+ try {
545
+ await this.deleteNamespaceValue(tableName, key);
546
+ } catch (error) {
547
+ this.logger.error(`Failed to delete KV value for ${tableName}:${key}:`, error);
548
+ throw new Error(`Failed to delete KV value: ${error.message}`);
549
+ }
550
+ }
551
+ async listKV(tableName, options) {
552
+ try {
553
+ return await this.listNamespaceKeys(tableName, options);
554
+ } catch (error) {
555
+ this.logger.error(`Failed to list KV for ${tableName}:`, error);
556
+ throw new Error(`Failed to list KV: ${error.message}`);
957
557
  }
958
- const updatedAt = /* @__PURE__ */ new Date();
959
- const updatedResource = {
960
- ...existingResource,
961
- workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
962
- metadata: {
963
- ...existingResource.metadata,
964
- ...metadata
965
- },
966
- updatedAt
967
- };
968
- return this.saveResource({ resource: updatedResource });
969
558
  }
970
559
  };
971
- var StoreOperationsCloudflare = class extends StoreOperations {
972
- bindings;
973
- client;
974
- accountId;
975
- namespacePrefix;
976
- constructor({
977
- namespacePrefix,
978
- bindings,
979
- client,
980
- accountId
981
- }) {
560
+
561
+ // src/storage/domains/memory/index.ts
562
+ var MemoryStorageCloudflare = class extends MemoryStorage {
563
+ #db;
564
+ constructor(config) {
982
565
  super();
983
- this.bindings = bindings;
984
- this.namespacePrefix = namespacePrefix;
985
- this.client = client;
986
- this.accountId = accountId;
566
+ this.#db = new CloudflareKVDB(resolveCloudflareConfig(config));
987
567
  }
988
- async hasColumn() {
989
- return true;
568
+ async init() {
990
569
  }
991
- async alterTable(_args) {
570
+ async dangerouslyClearAll() {
571
+ await this.#db.clearTable({ tableName: TABLE_MESSAGES });
572
+ await this.#db.clearTable({ tableName: TABLE_THREADS });
573
+ await this.#db.clearTable({ tableName: TABLE_RESOURCES });
992
574
  }
993
- async clearTable({ tableName }) {
575
+ ensureMetadata(metadata) {
576
+ if (!metadata) return void 0;
577
+ return typeof metadata === "string" ? JSON.parse(metadata) : metadata;
578
+ }
579
+ /**
580
+ * Summarizes message content without exposing raw data (for logging).
581
+ * Returns type, length, and keys only to prevent PII leakage.
582
+ */
583
+ summarizeMessageContent(content) {
584
+ if (!content) return { type: "undefined" };
585
+ if (typeof content === "string") return { type: "string", length: content.length };
586
+ if (Array.isArray(content)) return { type: "array", length: content.length };
587
+ if (typeof content === "object") return { type: "object", keys: Object.keys(content) };
588
+ return { type: typeof content };
589
+ }
590
+ async getThreadById({ threadId }) {
591
+ const thread = await this.#db.load({ tableName: TABLE_THREADS, keys: { id: threadId } });
592
+ if (!thread) return null;
994
593
  try {
995
- const keys = await this.listKV(tableName);
996
- if (keys.length > 0) {
997
- await Promise.all(keys.map((keyObj) => this.deleteKV(tableName, keyObj.name)));
998
- }
594
+ return {
595
+ ...thread,
596
+ createdAt: ensureDate(thread.createdAt),
597
+ updatedAt: ensureDate(thread.updatedAt),
598
+ metadata: this.ensureMetadata(thread.metadata)
599
+ };
999
600
  } catch (error) {
1000
- throw new MastraError(
601
+ const mastraError = new MastraError(
1001
602
  {
1002
- id: createStorageErrorId("CLOUDFLARE", "CLEAR_TABLE", "FAILED"),
603
+ id: createStorageErrorId("CLOUDFLARE", "GET_THREAD_BY_ID", "FAILED"),
1003
604
  domain: ErrorDomain.STORAGE,
1004
605
  category: ErrorCategory.THIRD_PARTY,
1005
606
  details: {
1006
- tableName
607
+ threadId
1007
608
  }
1008
609
  },
1009
610
  error
1010
611
  );
612
+ this.logger?.trackException(mastraError);
613
+ this.logger?.error(mastraError.toString());
614
+ return null;
1011
615
  }
1012
616
  }
1013
- async dropTable({ tableName }) {
617
+ async listThreadsByResourceId(args) {
1014
618
  try {
1015
- const keys = await this.listKV(tableName);
1016
- if (keys.length > 0) {
1017
- await Promise.all(keys.map((keyObj) => this.deleteKV(tableName, keyObj.name)));
619
+ const { resourceId, page = 0, perPage: perPageInput, orderBy } = args;
620
+ const perPage = normalizePerPage(perPageInput, 100);
621
+ if (page < 0) {
622
+ throw new MastraError(
623
+ {
624
+ id: createStorageErrorId("CLOUDFLARE", "LIST_THREADS_BY_RESOURCE_ID", "INVALID_PAGE"),
625
+ domain: ErrorDomain.STORAGE,
626
+ category: ErrorCategory.USER,
627
+ details: { page }
628
+ },
629
+ new Error("page must be >= 0")
630
+ );
1018
631
  }
1019
- } catch (error) {
1020
- throw new MastraError(
1021
- {
1022
- id: createStorageErrorId("CLOUDFLARE", "DROP_TABLE", "FAILED"),
1023
- domain: ErrorDomain.STORAGE,
1024
- category: ErrorCategory.THIRD_PARTY,
1025
- details: {
1026
- tableName
1027
- }
1028
- },
1029
- error
1030
- );
1031
- }
1032
- }
1033
- getBinding(tableName) {
1034
- if (!this.bindings) {
1035
- throw new Error(`Cannot use Workers API binding for ${tableName}: Store initialized with REST API configuration`);
1036
- }
1037
- const binding = this.bindings[tableName];
1038
- if (!binding) throw new Error(`No binding found for namespace ${tableName}`);
1039
- return binding;
1040
- }
1041
- getKey(tableName, record) {
1042
- const prefix = this.namespacePrefix ? `${this.namespacePrefix}:` : "";
1043
- switch (tableName) {
1044
- case TABLE_THREADS:
1045
- if (!record.id) throw new Error("Thread ID is required");
1046
- return `${prefix}${tableName}:${record.id}`;
1047
- case TABLE_MESSAGES:
1048
- if (!record.threadId || !record.id) throw new Error("Thread ID and Message ID are required");
1049
- return `${prefix}${tableName}:${record.threadId}:${record.id}`;
1050
- case TABLE_WORKFLOW_SNAPSHOT:
1051
- if (!record.workflow_name || !record.run_id) {
1052
- throw new Error("Workflow name, and run ID are required");
1053
- }
1054
- let key = `${prefix}${tableName}:${record.workflow_name}:${record.run_id}`;
1055
- if (record.resourceId) {
1056
- key = `${key}:${record.resourceId}`;
1057
- }
1058
- return key;
1059
- case TABLE_TRACES:
1060
- if (!record.id) throw new Error("Trace ID is required");
1061
- return `${prefix}${tableName}:${record.id}`;
1062
- case TABLE_SCORERS:
1063
- if (!record.id) throw new Error("Score ID is required");
1064
- return `${prefix}${tableName}:${record.id}`;
1065
- default:
1066
- throw new Error(`Unsupported table: ${tableName}`);
1067
- }
1068
- }
1069
- getSchemaKey(tableName) {
1070
- const prefix = this.namespacePrefix ? `${this.namespacePrefix}:` : "";
1071
- return `${prefix}schema:${tableName}`;
1072
- }
1073
- /**
1074
- * Helper to safely parse data from KV storage
1075
- */
1076
- safeParse(text) {
1077
- if (!text) return null;
1078
- try {
1079
- const data = JSON.parse(text);
1080
- if (data && typeof data === "object" && "value" in data) {
1081
- if (typeof data.value === "string") {
1082
- try {
1083
- return JSON.parse(data.value);
1084
- } catch {
1085
- return data.value;
1086
- }
1087
- }
1088
- return null;
632
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
633
+ const { field, direction } = this.parseOrderBy(orderBy);
634
+ const prefix = this.#db.namespacePrefix ? `${this.#db.namespacePrefix}:` : "";
635
+ const keyObjs = await this.#db.listKV(TABLE_THREADS, { prefix: `${prefix}${TABLE_THREADS}` });
636
+ const threads = [];
637
+ for (const { name: key } of keyObjs) {
638
+ const data = await this.#db.getKV(TABLE_THREADS, key);
639
+ if (!data) continue;
640
+ if (data.resourceId !== resourceId) continue;
641
+ threads.push(data);
1089
642
  }
1090
- return data;
1091
- } catch (error) {
1092
- const message = error instanceof Error ? error.message : String(error);
1093
- this.logger.error("Failed to parse text:", { message, text });
1094
- return null;
1095
- }
1096
- }
1097
- async createNamespaceById(title) {
1098
- if (this.bindings) {
643
+ threads.sort((a, b) => {
644
+ const aTime = new Date(a[field] || 0).getTime();
645
+ const bTime = new Date(b[field] || 0).getTime();
646
+ return direction === "ASC" ? aTime - bTime : bTime - aTime;
647
+ });
648
+ const end = perPageInput === false ? threads.length : offset + perPage;
649
+ const paginatedThreads = threads.slice(offset, end);
1099
650
  return {
1100
- id: title,
1101
- // Use title as ID since that's what we need
1102
- title,
1103
- supports_url_encoding: true
651
+ page,
652
+ perPage: perPageForResponse,
653
+ total: threads.length,
654
+ hasMore: perPageInput === false ? false : offset + perPage < threads.length,
655
+ threads: paginatedThreads
1104
656
  };
657
+ } catch (error) {
658
+ throw new MastraError(
659
+ {
660
+ id: createStorageErrorId("CLOUDFLARE", "LIST_THREADS_BY_RESOURCE_ID", "FAILED"),
661
+ domain: ErrorDomain.STORAGE,
662
+ category: ErrorCategory.THIRD_PARTY,
663
+ text: "Failed to get threads by resource ID with pagination"
664
+ },
665
+ error
666
+ );
1105
667
  }
1106
- return await this.client.kv.namespaces.create({
1107
- account_id: this.accountId,
1108
- title
1109
- });
1110
668
  }
1111
- async createNamespace(namespaceName) {
669
+ async saveThread({ thread }) {
1112
670
  try {
1113
- const response = await this.createNamespaceById(namespaceName);
1114
- return response.id;
671
+ await this.#db.insert({ tableName: TABLE_THREADS, record: thread });
672
+ return thread;
1115
673
  } catch (error) {
1116
- if (error.message && error.message.includes("already exists")) {
1117
- const namespaces = await this.listNamespaces();
1118
- const namespace = namespaces.result.find((ns) => ns.title === namespaceName);
1119
- if (namespace) return namespace.id;
1120
- }
1121
- this.logger.error("Error creating namespace:", error);
1122
- throw new Error(`Failed to create namespace ${namespaceName}: ${error.message}`);
1123
- }
1124
- }
1125
- async listNamespaces() {
1126
- if (this.bindings) {
1127
- return {
1128
- result: Object.keys(this.bindings).map((name) => ({
1129
- id: name,
1130
- title: name,
1131
- supports_url_encoding: true
1132
- }))
1133
- };
1134
- }
1135
- let allNamespaces = [];
1136
- let currentPage = 1;
1137
- const perPage = 50;
1138
- let morePagesExist = true;
1139
- while (morePagesExist) {
1140
- const response = await this.client.kv.namespaces.list({
1141
- account_id: this.accountId,
1142
- page: currentPage,
1143
- per_page: perPage
1144
- });
1145
- if (response.result) {
1146
- allNamespaces = allNamespaces.concat(response.result);
1147
- }
1148
- morePagesExist = response.result ? response.result.length === perPage : false;
1149
- if (morePagesExist) {
1150
- currentPage++;
1151
- }
674
+ throw new MastraError(
675
+ {
676
+ id: createStorageErrorId("CLOUDFLARE", "SAVE_THREAD", "FAILED"),
677
+ domain: ErrorDomain.STORAGE,
678
+ category: ErrorCategory.THIRD_PARTY,
679
+ details: {
680
+ threadId: thread.id
681
+ }
682
+ },
683
+ error
684
+ );
1152
685
  }
1153
- return { result: allNamespaces };
1154
686
  }
1155
- async getNamespaceIdByName(namespaceName) {
687
+ async updateThread({
688
+ id,
689
+ title,
690
+ metadata
691
+ }) {
1156
692
  try {
1157
- const response = await this.listNamespaces();
1158
- const namespace = response.result.find((ns) => ns.title === namespaceName);
1159
- return namespace ? namespace.id : null;
693
+ const thread = await this.getThreadById({ threadId: id });
694
+ if (!thread) {
695
+ throw new Error(`Thread ${id} not found`);
696
+ }
697
+ const updatedThread = {
698
+ ...thread,
699
+ title,
700
+ metadata: this.ensureMetadata({
701
+ ...thread.metadata ?? {},
702
+ ...metadata
703
+ }),
704
+ updatedAt: /* @__PURE__ */ new Date()
705
+ };
706
+ await this.#db.insert({ tableName: TABLE_THREADS, record: updatedThread });
707
+ return updatedThread;
1160
708
  } catch (error) {
1161
- this.logger.error(`Failed to get namespace ID for ${namespaceName}:`, error);
1162
- return null;
1163
- }
1164
- }
1165
- async getOrCreateNamespaceId(namespaceName) {
1166
- let namespaceId = await this.getNamespaceIdByName(namespaceName);
1167
- if (!namespaceId) {
1168
- namespaceId = await this.createNamespace(namespaceName);
709
+ throw new MastraError(
710
+ {
711
+ id: createStorageErrorId("CLOUDFLARE", "UPDATE_THREAD", "FAILED"),
712
+ domain: ErrorDomain.STORAGE,
713
+ category: ErrorCategory.THIRD_PARTY,
714
+ details: {
715
+ threadId: id,
716
+ title
717
+ }
718
+ },
719
+ error
720
+ );
1169
721
  }
1170
- return namespaceId;
1171
722
  }
1172
- async getNamespaceId(tableName) {
1173
- const prefix = this.namespacePrefix ? `${this.namespacePrefix}_` : "";
723
+ getMessageKey(threadId, messageId) {
1174
724
  try {
1175
- return await this.getOrCreateNamespaceId(`${prefix}${tableName}`);
725
+ return this.#db.getKey(TABLE_MESSAGES, { threadId, id: messageId });
1176
726
  } catch (error) {
1177
- this.logger.error("Error fetching namespace ID:", error);
1178
- throw new Error(`Failed to fetch namespace ID for table ${tableName}: ${error.message}`);
727
+ const message = error instanceof Error ? error.message : String(error);
728
+ this.logger?.error(`Error getting message key for thread ${threadId} and message ${messageId}:`, { message });
729
+ throw error;
1179
730
  }
1180
731
  }
1181
- async getNamespaceValue(tableName, key) {
732
+ getThreadMessagesKey(threadId) {
1182
733
  try {
1183
- if (this.bindings) {
1184
- const binding = this.getBinding(tableName);
1185
- const result = await binding.getWithMetadata(key, "text");
1186
- if (!result) return null;
1187
- return JSON.stringify(result);
1188
- } else {
1189
- const namespaceId = await this.getNamespaceId(tableName);
1190
- const response = await this.client.kv.namespaces.values.get(namespaceId, key, {
1191
- account_id: this.accountId
1192
- });
1193
- return await response.text();
1194
- }
734
+ return this.#db.getKey(TABLE_MESSAGES, { threadId, id: "messages" });
1195
735
  } catch (error) {
1196
- if (error.message && error.message.includes("key not found")) {
1197
- return null;
1198
- }
1199
736
  const message = error instanceof Error ? error.message : String(error);
1200
- this.logger.error(`Failed to get value for ${tableName} ${key}:`, { message });
737
+ this.logger?.error(`Error getting thread messages key for thread ${threadId}:`, { message });
1201
738
  throw error;
1202
739
  }
1203
740
  }
1204
- async getKV(tableName, key) {
741
+ async deleteThread({ threadId }) {
1205
742
  try {
1206
- const text = await this.getNamespaceValue(tableName, key);
1207
- return this.safeParse(text);
743
+ const thread = await this.getThreadById({ threadId });
744
+ if (!thread) {
745
+ throw new Error(`Thread ${threadId} not found`);
746
+ }
747
+ const messageKeys = await this.#db.listKV(TABLE_MESSAGES);
748
+ const threadMessageKeys = messageKeys.filter((key) => key.name.includes(`${TABLE_MESSAGES}:${threadId}:`));
749
+ await Promise.all([
750
+ // Delete message order
751
+ this.#db.deleteKV(TABLE_MESSAGES, this.getThreadMessagesKey(threadId)),
752
+ // Delete all messages
753
+ ...threadMessageKeys.map((key) => this.#db.deleteKV(TABLE_MESSAGES, key.name)),
754
+ // Delete thread
755
+ this.#db.deleteKV(TABLE_THREADS, this.#db.getKey(TABLE_THREADS, { id: threadId }))
756
+ ]);
1208
757
  } catch (error) {
1209
- this.logger.error(`Failed to get KV value for ${tableName}:${key}:`, error);
1210
- throw new Error(`Failed to get KV value: ${error.message}`);
758
+ throw new MastraError(
759
+ {
760
+ id: createStorageErrorId("CLOUDFLARE", "DELETE_THREAD", "FAILED"),
761
+ domain: ErrorDomain.STORAGE,
762
+ category: ErrorCategory.THIRD_PARTY,
763
+ details: {
764
+ threadId
765
+ }
766
+ },
767
+ error
768
+ );
1211
769
  }
1212
770
  }
1213
- async getTableSchema(tableName) {
1214
- try {
1215
- const schemaKey = this.getSchemaKey(tableName);
1216
- return await this.getKV(tableName, schemaKey);
1217
- } catch (error) {
1218
- const message = error instanceof Error ? error.message : String(error);
1219
- this.logger.error(`Failed to get schema for ${tableName}:`, { message });
771
+ /**
772
+ * Searches all threads in the KV store to find a message by its ID.
773
+ *
774
+ * **Performance Warning**: This method sequentially scans all threads to locate
775
+ * the message. For stores with many threads, this can result in significant
776
+ * latency and API calls. When possible, callers should provide the `threadId`
777
+ * directly to avoid this full scan.
778
+ *
779
+ * @param messageId - The globally unique message ID to search for
780
+ * @returns The message with its threadId if found, null otherwise
781
+ */
782
+ async findMessageInAnyThread(messageId) {
783
+ try {
784
+ const prefix = this.#db.namespacePrefix ? `${this.#db.namespacePrefix}:` : "";
785
+ const threadKeys = await this.#db.listKV(TABLE_THREADS, { prefix: `${prefix}${TABLE_THREADS}` });
786
+ for (const { name: threadKey } of threadKeys) {
787
+ const threadId = threadKey.split(":").pop();
788
+ if (!threadId || threadId === "messages") continue;
789
+ const messageKey = this.getMessageKey(threadId, messageId);
790
+ const message = await this.#db.getKV(TABLE_MESSAGES, messageKey);
791
+ if (message) {
792
+ return { ...message, threadId };
793
+ }
794
+ }
1220
795
  return null;
796
+ } catch (error) {
797
+ this.logger?.error(`Error finding message ${messageId} in any thread:`, error);
798
+ return null;
799
+ }
800
+ }
801
+ /**
802
+ * Queue for serializing sorted order updates.
803
+ * Updates the sorted order for a given key. This operation is eventually consistent.
804
+ */
805
+ updateQueue = /* @__PURE__ */ new Map();
806
+ async updateSorting(threadMessages) {
807
+ return threadMessages.map((msg) => ({
808
+ message: msg,
809
+ // Use _index if available, otherwise timestamp, matching Upstash
810
+ score: msg._index !== void 0 ? msg._index : msg.createdAt.getTime()
811
+ })).sort((a, b) => a.score - b.score).map((item) => ({
812
+ id: item.message.id,
813
+ score: item.score
814
+ }));
815
+ }
816
+ /**
817
+ * Updates the sorted order for a given key. This operation is eventually consistent.
818
+ * Note: Operations on the same orderKey are serialized using a queue to prevent
819
+ * concurrent updates from conflicting with each other.
820
+ */
821
+ async updateSortedMessages(orderKey, newEntries) {
822
+ const currentPromise = this.updateQueue.get(orderKey) || Promise.resolve();
823
+ const nextPromise = currentPromise.then(async () => {
824
+ try {
825
+ const currentOrder = await this.getSortedMessages(orderKey);
826
+ const orderMap = new Map(currentOrder.map((entry) => [entry.id, entry]));
827
+ for (const entry of newEntries) {
828
+ orderMap.set(entry.id, entry);
829
+ }
830
+ const updatedOrder = Array.from(orderMap.values()).sort((a, b) => a.score - b.score);
831
+ await this.#db.putKV({
832
+ tableName: TABLE_MESSAGES,
833
+ key: orderKey,
834
+ value: JSON.stringify(updatedOrder)
835
+ });
836
+ } catch (error) {
837
+ const message = error instanceof Error ? error.message : String(error);
838
+ this.logger?.error(`Error updating sorted order for key ${orderKey}:`, { message });
839
+ throw error;
840
+ } finally {
841
+ if (this.updateQueue.get(orderKey) === nextPromise) {
842
+ this.updateQueue.delete(orderKey);
843
+ }
844
+ }
845
+ });
846
+ this.updateQueue.set(orderKey, nextPromise);
847
+ return nextPromise;
848
+ }
849
+ async getSortedMessages(orderKey) {
850
+ const raw = await this.#db.getKV(TABLE_MESSAGES, orderKey);
851
+ if (!raw) return [];
852
+ try {
853
+ const arr = JSON.parse(typeof raw === "string" ? raw : JSON.stringify(raw));
854
+ return Array.isArray(arr) ? arr : [];
855
+ } catch (e) {
856
+ this.logger?.error(`Error parsing order data for key ${orderKey}:`, { e });
857
+ return [];
858
+ }
859
+ }
860
+ async migrateMessage(messageId, fromThreadId, toThreadId) {
861
+ try {
862
+ const oldMessageKey = this.getMessageKey(fromThreadId, messageId);
863
+ const message = await this.#db.getKV(TABLE_MESSAGES, oldMessageKey);
864
+ if (!message) return;
865
+ const updatedMessage = {
866
+ ...message,
867
+ threadId: toThreadId
868
+ };
869
+ const newMessageKey = this.getMessageKey(toThreadId, messageId);
870
+ await this.#db.putKV({ tableName: TABLE_MESSAGES, key: newMessageKey, value: updatedMessage });
871
+ const oldOrderKey = this.getThreadMessagesKey(fromThreadId);
872
+ const oldEntries = await this.getSortedMessages(oldOrderKey);
873
+ const filteredEntries = oldEntries.filter((entry) => entry.id !== messageId);
874
+ await this.updateSortedMessages(oldOrderKey, filteredEntries);
875
+ const newOrderKey = this.getThreadMessagesKey(toThreadId);
876
+ const newEntries = await this.getSortedMessages(newOrderKey);
877
+ const newEntry = { id: messageId, score: Date.now() };
878
+ newEntries.push(newEntry);
879
+ await this.updateSortedMessages(newOrderKey, newEntries);
880
+ await this.#db.deleteKV(TABLE_MESSAGES, oldMessageKey);
881
+ } catch (error) {
882
+ this.logger?.error(`Error migrating message ${messageId} from ${fromThreadId} to ${toThreadId}:`, error);
883
+ throw error;
884
+ }
885
+ }
886
+ async saveMessages(args) {
887
+ const { messages } = args;
888
+ if (!Array.isArray(messages) || messages.length === 0) return { messages: [] };
889
+ try {
890
+ const validatedMessages = messages.map((message, index) => {
891
+ const errors = [];
892
+ if (!message.id) errors.push("id is required");
893
+ if (!message.threadId) errors.push("threadId is required");
894
+ if (!message.content) errors.push("content is required");
895
+ if (!message.role) errors.push("role is required");
896
+ if (!message.createdAt) errors.push("createdAt is required");
897
+ if (message.resourceId === null || message.resourceId === void 0) errors.push("resourceId is required");
898
+ if (errors.length > 0) {
899
+ throw new Error(`Invalid message at index ${index}: ${errors.join(", ")}`);
900
+ }
901
+ return {
902
+ ...message,
903
+ createdAt: ensureDate(message.createdAt),
904
+ type: message.type || "v2",
905
+ _index: index
906
+ };
907
+ }).filter((m) => !!m);
908
+ const messageMigrationTasks = [];
909
+ for (const message of validatedMessages) {
910
+ const existingMessage = await this.findMessageInAnyThread(message.id);
911
+ this.logger?.debug(
912
+ `Checking message ${message.id}: existing=${existingMessage?.threadId}, new=${message.threadId}`
913
+ );
914
+ if (existingMessage && existingMessage.threadId && existingMessage.threadId !== message.threadId) {
915
+ this.logger?.debug(`Migrating message ${message.id} from ${existingMessage.threadId} to ${message.threadId}`);
916
+ messageMigrationTasks.push(this.migrateMessage(message.id, existingMessage.threadId, message.threadId));
917
+ }
918
+ }
919
+ await Promise.all(messageMigrationTasks);
920
+ const messagesByThread = validatedMessages.reduce((acc, message) => {
921
+ if (message.threadId && !acc.has(message.threadId)) {
922
+ acc.set(message.threadId, []);
923
+ }
924
+ if (message.threadId) {
925
+ acc.get(message.threadId).push(message);
926
+ }
927
+ return acc;
928
+ }, /* @__PURE__ */ new Map());
929
+ await Promise.all(
930
+ Array.from(messagesByThread.entries()).map(async ([threadId, threadMessages]) => {
931
+ try {
932
+ const thread = await this.getThreadById({ threadId });
933
+ if (!thread) {
934
+ throw new Error(`Thread ${threadId} not found`);
935
+ }
936
+ await Promise.all(
937
+ threadMessages.map(async (message) => {
938
+ const key = this.getMessageKey(threadId, message.id);
939
+ const { _index, ...cleanMessage } = message;
940
+ const serializedMessage = {
941
+ ...cleanMessage,
942
+ createdAt: serializeDate(cleanMessage.createdAt)
943
+ };
944
+ this.logger?.debug(`Saving message ${message.id}`, {
945
+ contentSummary: this.summarizeMessageContent(serializedMessage.content)
946
+ });
947
+ await this.#db.putKV({ tableName: TABLE_MESSAGES, key, value: serializedMessage });
948
+ })
949
+ );
950
+ const orderKey = this.getThreadMessagesKey(threadId);
951
+ const entries = await this.updateSorting(threadMessages);
952
+ await this.updateSortedMessages(orderKey, entries);
953
+ const updatedThread = {
954
+ ...thread,
955
+ updatedAt: /* @__PURE__ */ new Date()
956
+ };
957
+ await this.#db.putKV({
958
+ tableName: TABLE_THREADS,
959
+ key: this.#db.getKey(TABLE_THREADS, { id: threadId }),
960
+ value: updatedThread
961
+ });
962
+ } catch (error) {
963
+ throw new MastraError(
964
+ {
965
+ id: createStorageErrorId("CLOUDFLARE", "SAVE_MESSAGES", "FAILED"),
966
+ domain: ErrorDomain.STORAGE,
967
+ category: ErrorCategory.THIRD_PARTY,
968
+ details: {
969
+ threadId
970
+ }
971
+ },
972
+ error
973
+ );
974
+ }
975
+ })
976
+ );
977
+ const prepared = validatedMessages.map(
978
+ ({ _index, ...message }) => ({ ...message, type: message.type !== "v2" ? message.type : void 0 })
979
+ );
980
+ const list = new MessageList().add(prepared, "memory");
981
+ return { messages: list.get.all.db() };
982
+ } catch (error) {
983
+ throw new MastraError(
984
+ {
985
+ id: createStorageErrorId("CLOUDFLARE", "SAVE_MESSAGES", "FAILED"),
986
+ domain: ErrorDomain.STORAGE,
987
+ category: ErrorCategory.THIRD_PARTY
988
+ },
989
+ error
990
+ );
991
+ }
992
+ }
993
+ async getRank(orderKey, id) {
994
+ const order = await this.getSortedMessages(orderKey);
995
+ const index = order.findIndex((item) => item.id === id);
996
+ return index >= 0 ? index : null;
997
+ }
998
+ async getRange(orderKey, start, end) {
999
+ const order = await this.getSortedMessages(orderKey);
1000
+ const actualStart = start < 0 ? Math.max(0, order.length + start) : start;
1001
+ const actualEnd = end < 0 ? order.length + end : Math.min(end, order.length - 1);
1002
+ const sliced = order.slice(actualStart, actualEnd + 1);
1003
+ return sliced.map((item) => item.id);
1004
+ }
1005
+ async getLastN(orderKey, n) {
1006
+ return this.getRange(orderKey, -n, -1);
1007
+ }
1008
+ async getFullOrder(orderKey) {
1009
+ return this.getRange(orderKey, 0, -1);
1010
+ }
1011
+ /**
1012
+ * Retrieves messages specified in the include array along with their surrounding context.
1013
+ *
1014
+ * **Performance Note**: When `threadId` is not provided in an include entry, this method
1015
+ * must call `findMessageInAnyThread` which sequentially scans all threads in the KV store.
1016
+ * For optimal performance, callers should provide `threadId` in include entries when known.
1017
+ *
1018
+ * @param include - Array of message IDs to include, optionally with context windows
1019
+ * @param messageIds - Set to accumulate the message IDs that should be fetched
1020
+ */
1021
+ async getIncludedMessagesWithContext(include, messageIds) {
1022
+ await Promise.all(
1023
+ include.map(async (item) => {
1024
+ let targetThreadId = item.threadId;
1025
+ if (!targetThreadId) {
1026
+ const foundMessage = await this.findMessageInAnyThread(item.id);
1027
+ if (!foundMessage) return;
1028
+ targetThreadId = foundMessage.threadId;
1029
+ }
1030
+ if (!targetThreadId) return;
1031
+ const threadMessagesKey = this.getThreadMessagesKey(targetThreadId);
1032
+ messageIds.add(item.id);
1033
+ if (!item.withPreviousMessages && !item.withNextMessages) return;
1034
+ const rank = await this.getRank(threadMessagesKey, item.id);
1035
+ if (rank === null) return;
1036
+ if (item.withPreviousMessages) {
1037
+ const prevIds = await this.getRange(
1038
+ threadMessagesKey,
1039
+ Math.max(0, rank - item.withPreviousMessages),
1040
+ rank - 1
1041
+ );
1042
+ prevIds.forEach((id) => messageIds.add(id));
1043
+ }
1044
+ if (item.withNextMessages) {
1045
+ const nextIds = await this.getRange(threadMessagesKey, rank + 1, rank + item.withNextMessages);
1046
+ nextIds.forEach((id) => messageIds.add(id));
1047
+ }
1048
+ })
1049
+ );
1050
+ }
1051
+ async getRecentMessages(threadId, limit, messageIds) {
1052
+ if (!threadId.trim()) throw new Error("threadId must be a non-empty string");
1053
+ if (limit <= 0) return;
1054
+ try {
1055
+ const threadMessagesKey = this.getThreadMessagesKey(threadId);
1056
+ const latestIds = await this.getLastN(threadMessagesKey, limit);
1057
+ latestIds.forEach((id) => messageIds.add(id));
1058
+ } catch {
1059
+ this.logger?.debug(`No message order found for thread ${threadId}, skipping latest messages`);
1221
1060
  }
1222
1061
  }
1223
- validateColumnValue(value, column) {
1224
- if (value === void 0 || value === null) {
1225
- return column.nullable ?? false;
1062
+ /**
1063
+ * Fetches and parses messages from one or more threads.
1064
+ *
1065
+ * **Performance Note**: When neither `include` entries with `threadId` nor `targetThreadId`
1066
+ * are provided, this method falls back to `findMessageInAnyThread` which scans all threads.
1067
+ * For optimal performance, provide `threadId` in include entries or specify `targetThreadId`.
1068
+ */
1069
+ async fetchAndParseMessagesFromMultipleThreads(messageIds, include, targetThreadId) {
1070
+ const messageIdToThreadId = /* @__PURE__ */ new Map();
1071
+ if (include) {
1072
+ for (const item of include) {
1073
+ if (item.threadId) {
1074
+ messageIdToThreadId.set(item.id, item.threadId);
1075
+ }
1076
+ }
1226
1077
  }
1227
- switch (column.type) {
1228
- case "text":
1229
- case "uuid":
1230
- return typeof value === "string";
1231
- case "integer":
1232
- case "bigint":
1233
- return typeof value === "number";
1234
- case "timestamp":
1235
- return value instanceof Date || typeof value === "string" && !isNaN(Date.parse(value));
1236
- case "jsonb":
1237
- if (typeof value !== "object") return false;
1078
+ const messages = await Promise.all(
1079
+ messageIds.map(async (id) => {
1238
1080
  try {
1239
- JSON.stringify(value);
1240
- return true;
1241
- } catch {
1242
- return false;
1081
+ let threadId = messageIdToThreadId.get(id);
1082
+ if (!threadId) {
1083
+ if (targetThreadId) {
1084
+ threadId = targetThreadId;
1085
+ } else {
1086
+ const foundMessage = await this.findMessageInAnyThread(id);
1087
+ if (foundMessage) {
1088
+ threadId = foundMessage.threadId;
1089
+ }
1090
+ }
1091
+ }
1092
+ if (!threadId) return null;
1093
+ const key = this.getMessageKey(threadId, id);
1094
+ const data = await this.#db.getKV(TABLE_MESSAGES, key);
1095
+ if (!data) return null;
1096
+ const parsed = typeof data === "string" ? JSON.parse(data) : data;
1097
+ this.logger?.debug(`Retrieved message ${id} from thread ${threadId}`, {
1098
+ contentSummary: this.summarizeMessageContent(parsed.content)
1099
+ });
1100
+ return parsed;
1101
+ } catch (error) {
1102
+ const message = error instanceof Error ? error.message : String(error);
1103
+ this.logger?.error(`Error retrieving message ${id}:`, { message });
1104
+ return null;
1243
1105
  }
1244
- default:
1245
- return false;
1106
+ })
1107
+ );
1108
+ return messages.filter((msg) => msg !== null);
1109
+ }
1110
+ /**
1111
+ * Retrieves messages by their IDs.
1112
+ *
1113
+ * **Performance Warning**: This method calls `findMessageInAnyThread` for each message ID,
1114
+ * which scans all threads in the KV store. For large numbers of messages or threads,
1115
+ * this can result in significant latency. Consider using `listMessages` with specific
1116
+ * thread IDs when the thread context is known.
1117
+ */
1118
+ async listMessagesById({ messageIds }) {
1119
+ if (messageIds.length === 0) return { messages: [] };
1120
+ try {
1121
+ const messages = (await Promise.all(messageIds.map((id) => this.findMessageInAnyThread(id)))).filter(
1122
+ (result) => !!result
1123
+ );
1124
+ const prepared = messages.map(({ _index, ...message }) => ({
1125
+ ...message,
1126
+ ...message.type !== `v2` && { type: message.type },
1127
+ createdAt: ensureDate(message.createdAt)
1128
+ }));
1129
+ const list = new MessageList().add(prepared, "memory");
1130
+ return { messages: list.get.all.db() };
1131
+ } catch (error) {
1132
+ const mastraError = new MastraError(
1133
+ {
1134
+ id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES_BY_ID", "FAILED"),
1135
+ domain: ErrorDomain.STORAGE,
1136
+ category: ErrorCategory.THIRD_PARTY,
1137
+ text: `Error retrieving messages by ID`,
1138
+ details: {
1139
+ messageIds: JSON.stringify(messageIds)
1140
+ }
1141
+ },
1142
+ error
1143
+ );
1144
+ this.logger?.trackException(mastraError);
1145
+ this.logger?.error(mastraError.toString());
1146
+ return { messages: [] };
1246
1147
  }
1247
1148
  }
1248
- async validateAgainstSchema(record, schema) {
1149
+ async listMessages(args) {
1150
+ const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
1151
+ const threadIds = Array.isArray(threadId) ? threadId : [threadId];
1152
+ const isValidThreadId = (id) => typeof id === "string" && id.trim().length > 0;
1153
+ if (threadIds.length === 0 || threadIds.some((id) => !isValidThreadId(id))) {
1154
+ throw new MastraError(
1155
+ {
1156
+ id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES", "INVALID_THREAD_ID"),
1157
+ domain: ErrorDomain.STORAGE,
1158
+ category: ErrorCategory.THIRD_PARTY,
1159
+ details: { threadId: Array.isArray(threadId) ? JSON.stringify(threadId) : String(threadId) }
1160
+ },
1161
+ new Error("threadId must be a non-empty string or array of non-empty strings")
1162
+ );
1163
+ }
1164
+ const perPage = normalizePerPage(perPageInput, 40);
1165
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1249
1166
  try {
1250
- if (!schema || typeof schema !== "object" || schema.value === null) {
1251
- throw new Error("Invalid schema format");
1167
+ if (page < 0) {
1168
+ throw new MastraError(
1169
+ {
1170
+ id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES", "INVALID_PAGE"),
1171
+ domain: ErrorDomain.STORAGE,
1172
+ category: ErrorCategory.USER,
1173
+ details: { page }
1174
+ },
1175
+ new Error("page must be >= 0")
1176
+ );
1252
1177
  }
1253
- for (const [columnName, column] of Object.entries(schema)) {
1254
- const value = record[columnName];
1255
- if (column.primaryKey && (value === void 0 || value === null)) {
1256
- throw new Error(`Missing primary key value for column ${columnName}`);
1178
+ const { field, direction } = this.parseOrderBy(orderBy, "ASC");
1179
+ const threadMessageIds = /* @__PURE__ */ new Set();
1180
+ for (const tid of threadIds) {
1181
+ try {
1182
+ const threadMessagesKey = this.getThreadMessagesKey(tid);
1183
+ const allIds = await this.getFullOrder(threadMessagesKey);
1184
+ allIds.forEach((id) => threadMessageIds.add(id));
1185
+ } catch {
1257
1186
  }
1258
- if (!this.validateColumnValue(value, column)) {
1259
- const valueType = value === null ? "null" : typeof value;
1260
- throw new Error(`Invalid value for column ${columnName}: expected ${column.type}, got ${valueType}`);
1187
+ }
1188
+ const threadMessages = await this.fetchAndParseMessagesFromMultipleThreads(
1189
+ Array.from(threadMessageIds),
1190
+ void 0,
1191
+ threadIds.length === 1 ? threadIds[0] : void 0
1192
+ );
1193
+ let filteredThreadMessages = threadMessages;
1194
+ if (resourceId) {
1195
+ filteredThreadMessages = filteredThreadMessages.filter((msg) => msg.resourceId === resourceId);
1196
+ }
1197
+ const dateRange = filter?.dateRange;
1198
+ if (dateRange) {
1199
+ filteredThreadMessages = filteredThreadMessages.filter((msg) => {
1200
+ const messageDate = new Date(msg.createdAt);
1201
+ if (dateRange.start && messageDate < new Date(dateRange.start)) return false;
1202
+ if (dateRange.end && messageDate > new Date(dateRange.end)) return false;
1203
+ return true;
1204
+ });
1205
+ }
1206
+ const total = filteredThreadMessages.length;
1207
+ if (perPage === 0 && (!include || include.length === 0)) {
1208
+ return {
1209
+ messages: [],
1210
+ total,
1211
+ page,
1212
+ perPage: perPageForResponse,
1213
+ hasMore: offset < total
1214
+ };
1215
+ }
1216
+ filteredThreadMessages.sort((a, b) => {
1217
+ const timeA = new Date(a.createdAt).getTime();
1218
+ const timeB = new Date(b.createdAt).getTime();
1219
+ const timeDiff = direction === "ASC" ? timeA - timeB : timeB - timeA;
1220
+ if (timeDiff === 0) {
1221
+ return a.id.localeCompare(b.id);
1261
1222
  }
1223
+ return timeDiff;
1224
+ });
1225
+ let paginatedMessages;
1226
+ if (perPage === 0) {
1227
+ paginatedMessages = [];
1228
+ } else if (perPage === Number.MAX_SAFE_INTEGER) {
1229
+ paginatedMessages = filteredThreadMessages;
1230
+ } else {
1231
+ paginatedMessages = filteredThreadMessages.slice(offset, offset + perPage);
1262
1232
  }
1263
- } catch (error) {
1264
- const message = error instanceof Error ? error.message : String(error);
1265
- this.logger.error(`Error validating record against schema:`, { message, record, schema });
1266
- throw error;
1267
- }
1268
- }
1269
- async validateRecord(record, tableName) {
1270
- try {
1271
- if (!record || typeof record !== "object") {
1272
- throw new Error("Record must be an object");
1233
+ let includedMessages = [];
1234
+ if (include && include.length > 0) {
1235
+ const includedMessageIds = /* @__PURE__ */ new Set();
1236
+ await this.getIncludedMessagesWithContext(include, includedMessageIds);
1237
+ const paginatedIds = new Set(paginatedMessages.map((m) => m.id));
1238
+ const idsToFetch = Array.from(includedMessageIds).filter((id) => !paginatedIds.has(id));
1239
+ if (idsToFetch.length > 0) {
1240
+ includedMessages = await this.fetchAndParseMessagesFromMultipleThreads(idsToFetch, include, void 0);
1241
+ }
1273
1242
  }
1274
- const recordTyped = record;
1275
- const schema = await this.getTableSchema(tableName);
1276
- if (schema) {
1277
- await this.validateAgainstSchema(recordTyped, schema);
1278
- return;
1243
+ const seenIds = /* @__PURE__ */ new Set();
1244
+ const allMessages = [];
1245
+ for (const msg of paginatedMessages) {
1246
+ if (!seenIds.has(msg.id)) {
1247
+ allMessages.push(msg);
1248
+ seenIds.add(msg.id);
1249
+ }
1279
1250
  }
1280
- switch (tableName) {
1281
- case TABLE_THREADS:
1282
- if (!("id" in recordTyped) || !("resourceId" in recordTyped) || !("title" in recordTyped)) {
1283
- throw new Error("Thread record missing required fields");
1284
- }
1285
- break;
1286
- case TABLE_MESSAGES:
1287
- if (!("id" in recordTyped) || !("threadId" in recordTyped) || !("content" in recordTyped) || !("role" in recordTyped)) {
1288
- throw new Error("Message record missing required fields");
1289
- }
1290
- break;
1291
- case TABLE_WORKFLOW_SNAPSHOT:
1292
- if (!("workflow_name" in recordTyped) || !("run_id" in recordTyped)) {
1293
- throw new Error("Workflow record missing required fields");
1294
- }
1295
- break;
1296
- case TABLE_TRACES:
1297
- if (!("id" in recordTyped)) {
1298
- throw new Error("Trace record missing required fields");
1299
- }
1300
- break;
1301
- case TABLE_SCORERS:
1302
- if (!("id" in recordTyped) || !("scorerId" in recordTyped)) {
1303
- throw new Error("Score record missing required fields");
1304
- }
1305
- break;
1306
- default:
1307
- throw new Error(`Unknown table type: ${tableName}`);
1251
+ for (const msg of includedMessages) {
1252
+ if (!seenIds.has(msg.id)) {
1253
+ allMessages.push(msg);
1254
+ seenIds.add(msg.id);
1255
+ }
1308
1256
  }
1309
- } catch (error) {
1310
- const message = error instanceof Error ? error.message : String(error);
1311
- this.logger.error(`Failed to validate record for ${tableName}:`, { message, record });
1312
- throw error;
1313
- }
1314
- }
1315
- async insert({ tableName, record }) {
1316
- try {
1317
- const key = this.getKey(tableName, record);
1318
- const processedRecord = { ...record };
1319
- await this.validateRecord(processedRecord, tableName);
1320
- await this.putKV({ tableName, key, value: processedRecord });
1321
- } catch (error) {
1322
- throw new MastraError(
1323
- {
1324
- id: createStorageErrorId("CLOUDFLARE", "INSERT", "FAILED"),
1325
- domain: ErrorDomain.STORAGE,
1326
- category: ErrorCategory.THIRD_PARTY,
1327
- details: {
1328
- tableName
1329
- }
1330
- },
1331
- error
1257
+ allMessages.sort((a, b) => {
1258
+ const timeA = new Date(a.createdAt).getTime();
1259
+ const timeB = new Date(b.createdAt).getTime();
1260
+ const timeDiff = direction === "ASC" ? timeA - timeB : timeB - timeA;
1261
+ if (timeDiff === 0) {
1262
+ return a.id.localeCompare(b.id);
1263
+ }
1264
+ return timeDiff;
1265
+ });
1266
+ let filteredMessages = allMessages;
1267
+ const paginatedCount = paginatedMessages.length;
1268
+ if (total === 0 && filteredMessages.length === 0 && (!include || include.length === 0)) {
1269
+ return {
1270
+ messages: [],
1271
+ total: 0,
1272
+ page,
1273
+ perPage: perPageForResponse,
1274
+ hasMore: false
1275
+ };
1276
+ }
1277
+ const prepared = filteredMessages.map(({ _index, ...message }) => ({
1278
+ ...message,
1279
+ type: message.type !== "v2" ? message.type : void 0,
1280
+ createdAt: ensureDate(message.createdAt)
1281
+ }));
1282
+ const primaryThreadId = Array.isArray(threadId) ? threadId[0] : threadId;
1283
+ const list = new MessageList({ threadId: primaryThreadId, resourceId }).add(
1284
+ prepared,
1285
+ "memory"
1332
1286
  );
1333
- }
1334
- }
1335
- async load({ tableName, keys }) {
1336
- try {
1337
- const key = this.getKey(tableName, keys);
1338
- const data = await this.getKV(tableName, key);
1339
- if (!data) return null;
1340
- return data;
1287
+ let finalMessages = list.get.all.db();
1288
+ finalMessages = finalMessages.sort((a, b) => {
1289
+ const isDateField = field === "createdAt" || field === "updatedAt";
1290
+ const aVal = isDateField ? new Date(a[field]).getTime() : a[field];
1291
+ const bVal = isDateField ? new Date(b[field]).getTime() : b[field];
1292
+ if (aVal == null && bVal == null) return a.id.localeCompare(b.id);
1293
+ if (aVal == null) return 1;
1294
+ if (bVal == null) return -1;
1295
+ if (typeof aVal === "number" && typeof bVal === "number") {
1296
+ const cmp2 = direction === "ASC" ? aVal - bVal : bVal - aVal;
1297
+ return cmp2 !== 0 ? cmp2 : a.id.localeCompare(b.id);
1298
+ }
1299
+ const cmp = direction === "ASC" ? String(aVal).localeCompare(String(bVal)) : String(bVal).localeCompare(String(aVal));
1300
+ return cmp !== 0 ? cmp : a.id.localeCompare(b.id);
1301
+ });
1302
+ const threadIdSet = new Set(threadIds);
1303
+ const returnedThreadMessageIds = new Set(
1304
+ finalMessages.filter((m) => m.threadId && threadIdSet.has(m.threadId)).map((m) => m.id)
1305
+ );
1306
+ const allThreadMessagesReturned = returnedThreadMessageIds.size >= total;
1307
+ const hasMore = perPageInput !== false && !allThreadMessagesReturned && offset + paginatedCount < total;
1308
+ return {
1309
+ messages: finalMessages,
1310
+ total,
1311
+ page,
1312
+ perPage: perPageForResponse,
1313
+ hasMore
1314
+ };
1341
1315
  } catch (error) {
1342
1316
  const mastraError = new MastraError(
1343
1317
  {
1344
- id: createStorageErrorId("CLOUDFLARE", "LOAD", "FAILED"),
1318
+ id: createStorageErrorId("CLOUDFLARE", "LIST_MESSAGES", "FAILED"),
1345
1319
  domain: ErrorDomain.STORAGE,
1346
1320
  category: ErrorCategory.THIRD_PARTY,
1321
+ text: `Failed to list messages for thread ${Array.isArray(threadId) ? threadId.join(",") : threadId}: ${error instanceof Error ? error.message : String(error)}`,
1347
1322
  details: {
1348
- tableName
1323
+ threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
1324
+ resourceId: resourceId ?? ""
1349
1325
  }
1350
1326
  },
1351
1327
  error
1352
1328
  );
1353
- this.logger?.trackException(mastraError);
1354
- this.logger?.error(mastraError.toString());
1355
- return null;
1329
+ this.logger?.error?.(mastraError.toString());
1330
+ this.logger?.trackException?.(mastraError);
1331
+ return {
1332
+ messages: [],
1333
+ total: 0,
1334
+ page,
1335
+ perPage: perPageForResponse,
1336
+ hasMore: false
1337
+ };
1356
1338
  }
1357
1339
  }
1358
- async batchInsert(input) {
1359
- if (!input.records || input.records.length === 0) return;
1340
+ async updateMessages(args) {
1360
1341
  try {
1361
- await Promise.all(
1362
- input.records.map(async (record) => {
1363
- const key = this.getKey(input.tableName, record);
1364
- await this.putKV({ tableName: input.tableName, key, value: record });
1365
- })
1366
- );
1342
+ const { messages } = args;
1343
+ const updatedMessages = [];
1344
+ for (const messageUpdate of messages) {
1345
+ const { id, content, ...otherFields } = messageUpdate;
1346
+ const prefix = this.#db.namespacePrefix ? `${this.#db.namespacePrefix}:` : "";
1347
+ const keyObjs = await this.#db.listKV(TABLE_MESSAGES, { prefix: `${prefix}${TABLE_MESSAGES}` });
1348
+ let existingMessage = null;
1349
+ let messageKey = "";
1350
+ for (const { name: key } of keyObjs) {
1351
+ const data = await this.#db.getKV(TABLE_MESSAGES, key);
1352
+ if (data && data.id === id) {
1353
+ existingMessage = data;
1354
+ messageKey = key;
1355
+ break;
1356
+ }
1357
+ }
1358
+ if (!existingMessage) {
1359
+ continue;
1360
+ }
1361
+ const updatedMessage = {
1362
+ ...existingMessage,
1363
+ ...otherFields,
1364
+ id
1365
+ };
1366
+ if (content) {
1367
+ if (content.metadata !== void 0) {
1368
+ updatedMessage.content = {
1369
+ ...updatedMessage.content,
1370
+ metadata: {
1371
+ ...updatedMessage.content?.metadata,
1372
+ ...content.metadata
1373
+ }
1374
+ };
1375
+ }
1376
+ if (content.content !== void 0) {
1377
+ updatedMessage.content = {
1378
+ ...updatedMessage.content,
1379
+ content: content.content
1380
+ };
1381
+ }
1382
+ }
1383
+ if ("threadId" in messageUpdate && messageUpdate.threadId && messageUpdate.threadId !== existingMessage.threadId) {
1384
+ await this.#db.deleteKV(TABLE_MESSAGES, messageKey);
1385
+ updatedMessage.threadId = messageUpdate.threadId;
1386
+ const newMessageKey = this.getMessageKey(messageUpdate.threadId, id);
1387
+ await this.#db.putKV({
1388
+ tableName: TABLE_MESSAGES,
1389
+ key: newMessageKey,
1390
+ value: updatedMessage
1391
+ });
1392
+ if (existingMessage.threadId) {
1393
+ const sourceOrderKey = this.getThreadMessagesKey(existingMessage.threadId);
1394
+ const sourceEntries = await this.getSortedMessages(sourceOrderKey);
1395
+ const filteredEntries = sourceEntries.filter((entry) => entry.id !== id);
1396
+ await this.updateSortedMessages(sourceOrderKey, filteredEntries);
1397
+ }
1398
+ const destOrderKey = this.getThreadMessagesKey(messageUpdate.threadId);
1399
+ const destEntries = await this.getSortedMessages(destOrderKey);
1400
+ const newEntry = { id, score: Date.now() };
1401
+ destEntries.push(newEntry);
1402
+ await this.updateSortedMessages(destOrderKey, destEntries);
1403
+ } else {
1404
+ await this.#db.putKV({
1405
+ tableName: TABLE_MESSAGES,
1406
+ key: messageKey,
1407
+ value: updatedMessage
1408
+ });
1409
+ }
1410
+ const threadsToUpdate = /* @__PURE__ */ new Set();
1411
+ if (updatedMessage.threadId) {
1412
+ threadsToUpdate.add(updatedMessage.threadId);
1413
+ }
1414
+ if ("threadId" in messageUpdate && messageUpdate.threadId && messageUpdate.threadId !== existingMessage.threadId) {
1415
+ if (existingMessage.threadId) {
1416
+ threadsToUpdate.add(existingMessage.threadId);
1417
+ }
1418
+ threadsToUpdate.add(messageUpdate.threadId);
1419
+ }
1420
+ for (const threadId of threadsToUpdate) {
1421
+ const thread = await this.getThreadById({ threadId });
1422
+ if (thread) {
1423
+ const updatedThread = {
1424
+ ...thread,
1425
+ updatedAt: /* @__PURE__ */ new Date()
1426
+ };
1427
+ await this.#db.putKV({
1428
+ tableName: TABLE_THREADS,
1429
+ key: this.#db.getKey(TABLE_THREADS, { id: threadId }),
1430
+ value: updatedThread
1431
+ });
1432
+ }
1433
+ }
1434
+ updatedMessages.push(updatedMessage);
1435
+ }
1436
+ return updatedMessages;
1367
1437
  } catch (error) {
1368
1438
  throw new MastraError(
1369
1439
  {
1370
- id: createStorageErrorId("CLOUDFLARE", "BATCH_INSERT", "FAILED"),
1440
+ id: createStorageErrorId("CLOUDFLARE", "UPDATE_MESSAGES", "FAILED"),
1371
1441
  domain: ErrorDomain.STORAGE,
1372
- category: ErrorCategory.THIRD_PARTY,
1373
- text: `Error in batch insert for table ${input.tableName}`,
1374
- details: {
1375
- tableName: input.tableName
1376
- }
1377
- },
1378
- error
1379
- );
1380
- }
1381
- }
1382
- /**
1383
- * Helper to safely serialize data for KV storage
1384
- */
1385
- safeSerialize(data) {
1386
- return typeof data === "string" ? data : JSON.stringify(data);
1387
- }
1388
- async putNamespaceValue({
1389
- tableName,
1390
- key,
1391
- value,
1392
- metadata
1393
- }) {
1394
- try {
1395
- const serializedValue = this.safeSerialize(value);
1396
- const serializedMetadata = metadata ? this.safeSerialize(metadata) : "";
1397
- if (this.bindings) {
1398
- const binding = this.getBinding(tableName);
1399
- await binding.put(key, serializedValue, { metadata: serializedMetadata });
1400
- } else {
1401
- const namespaceId = await this.getNamespaceId(tableName);
1402
- await this.client.kv.namespaces.values.update(namespaceId, key, {
1403
- account_id: this.accountId,
1404
- value: serializedValue,
1405
- metadata: serializedMetadata
1406
- });
1407
- }
1408
- } catch (error) {
1409
- const message = error instanceof Error ? error.message : String(error);
1410
- this.logger.error(`Failed to put value for ${tableName} ${key}:`, { message });
1411
- throw error;
1412
- }
1413
- }
1414
- async putKV({
1415
- tableName,
1416
- key,
1417
- value,
1418
- metadata
1419
- }) {
1420
- try {
1421
- await this.putNamespaceValue({ tableName, key, value, metadata });
1422
- } catch (error) {
1423
- this.logger.error(`Failed to put KV value for ${tableName}:${key}:`, error);
1424
- throw new Error(`Failed to put KV value: ${error.message}`);
1442
+ category: ErrorCategory.THIRD_PARTY,
1443
+ text: "Failed to update messages"
1444
+ },
1445
+ error
1446
+ );
1425
1447
  }
1426
1448
  }
1427
- async createTable({
1428
- tableName,
1429
- schema
1430
- }) {
1449
+ async getResourceById({ resourceId }) {
1431
1450
  try {
1432
- const schemaKey = this.getSchemaKey(tableName);
1433
- const metadata = {
1434
- type: "table_schema",
1435
- tableName,
1436
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1451
+ const data = await this.#db.getKV(TABLE_RESOURCES, resourceId);
1452
+ if (!data) return null;
1453
+ const resource = typeof data === "string" ? JSON.parse(data) : data;
1454
+ return {
1455
+ ...resource,
1456
+ createdAt: ensureDate(resource.createdAt),
1457
+ updatedAt: ensureDate(resource.updatedAt),
1458
+ metadata: this.ensureMetadata(resource.metadata)
1437
1459
  };
1438
- await this.putKV({ tableName, key: schemaKey, value: schema, metadata });
1439
1460
  } catch (error) {
1440
- throw new MastraError(
1461
+ const mastraError = new MastraError(
1441
1462
  {
1442
- id: createStorageErrorId("CLOUDFLARE", "CREATE_TABLE", "FAILED"),
1463
+ id: createStorageErrorId("CLOUDFLARE", "GET_RESOURCE_BY_ID", "FAILED"),
1443
1464
  domain: ErrorDomain.STORAGE,
1444
1465
  category: ErrorCategory.THIRD_PARTY,
1445
1466
  details: {
1446
- tableName
1467
+ resourceId
1447
1468
  }
1448
1469
  },
1449
1470
  error
1450
1471
  );
1472
+ this.logger?.trackException(mastraError);
1473
+ this.logger?.error(mastraError.toString());
1474
+ return null;
1451
1475
  }
1452
1476
  }
1453
- async listNamespaceKeys(tableName, options) {
1477
+ async saveResource({ resource }) {
1454
1478
  try {
1455
- if (this.bindings) {
1456
- const binding = this.getBinding(tableName);
1457
- const response = await binding.list({
1458
- limit: options?.limit || 1e3,
1459
- prefix: options?.prefix
1460
- });
1461
- return response.keys;
1462
- } else {
1463
- const namespaceId = await this.getNamespaceId(tableName);
1464
- const response = await this.client.kv.namespaces.keys.list(namespaceId, {
1465
- account_id: this.accountId,
1466
- limit: options?.limit || 1e3,
1467
- prefix: options?.prefix
1468
- });
1469
- return response.result;
1470
- }
1479
+ const resourceToSave = {
1480
+ ...resource,
1481
+ metadata: resource.metadata ? JSON.stringify(resource.metadata) : null
1482
+ };
1483
+ await this.#db.putKV({
1484
+ tableName: TABLE_RESOURCES,
1485
+ key: resource.id,
1486
+ value: resourceToSave
1487
+ });
1488
+ return resource;
1471
1489
  } catch (error) {
1472
1490
  throw new MastraError(
1473
1491
  {
1474
- id: createStorageErrorId("CLOUDFLARE", "LIST_NAMESPACE_KEYS", "FAILED"),
1492
+ id: createStorageErrorId("CLOUDFLARE", "SAVE_RESOURCE", "FAILED"),
1475
1493
  domain: ErrorDomain.STORAGE,
1476
1494
  category: ErrorCategory.THIRD_PARTY,
1477
1495
  details: {
1478
- tableName
1496
+ resourceId: resource.id
1479
1497
  }
1480
1498
  },
1481
1499
  error
1482
1500
  );
1483
1501
  }
1484
1502
  }
1485
- async deleteNamespaceValue(tableName, key) {
1486
- if (this.bindings) {
1487
- const binding = this.getBinding(tableName);
1488
- await binding.delete(key);
1489
- } else {
1490
- const namespaceId = await this.getNamespaceId(tableName);
1491
- await this.client.kv.namespaces.values.delete(namespaceId, key, {
1492
- account_id: this.accountId
1493
- });
1494
- }
1495
- }
1496
- async deleteKV(tableName, key) {
1497
- try {
1498
- await this.deleteNamespaceValue(tableName, key);
1499
- } catch (error) {
1500
- this.logger.error(`Failed to delete KV value for ${tableName}:${key}:`, error);
1501
- throw new Error(`Failed to delete KV value: ${error.message}`);
1503
+ async updateResource({
1504
+ resourceId,
1505
+ workingMemory,
1506
+ metadata
1507
+ }) {
1508
+ const existingResource = await this.getResourceById({ resourceId });
1509
+ if (!existingResource) {
1510
+ const newResource = {
1511
+ id: resourceId,
1512
+ workingMemory,
1513
+ metadata: metadata || {},
1514
+ createdAt: /* @__PURE__ */ new Date(),
1515
+ updatedAt: /* @__PURE__ */ new Date()
1516
+ };
1517
+ return this.saveResource({ resource: newResource });
1502
1518
  }
1519
+ const updatedAt = /* @__PURE__ */ new Date();
1520
+ const updatedResource = {
1521
+ ...existingResource,
1522
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1523
+ metadata: {
1524
+ ...existingResource.metadata,
1525
+ ...metadata
1526
+ },
1527
+ updatedAt
1528
+ };
1529
+ return this.saveResource({ resource: updatedResource });
1503
1530
  }
1504
- async listKV(tableName, options) {
1531
+ async deleteMessages(messageIds) {
1532
+ if (messageIds.length === 0) return;
1505
1533
  try {
1506
- return await this.listNamespaceKeys(tableName, options);
1534
+ const threadIds = /* @__PURE__ */ new Set();
1535
+ for (const messageId of messageIds) {
1536
+ const message = await this.findMessageInAnyThread(messageId);
1537
+ if (message?.threadId) {
1538
+ threadIds.add(message.threadId);
1539
+ const messageKey = this.getMessageKey(message.threadId, messageId);
1540
+ await this.#db.deleteKV(TABLE_MESSAGES, messageKey);
1541
+ const orderKey = this.getThreadMessagesKey(message.threadId);
1542
+ const entries = await this.getSortedMessages(orderKey);
1543
+ const filteredEntries = entries.filter((entry) => entry.id !== messageId);
1544
+ if (filteredEntries.length > 0) {
1545
+ await this.#db.putKV({
1546
+ tableName: TABLE_MESSAGES,
1547
+ key: orderKey,
1548
+ value: JSON.stringify(filteredEntries)
1549
+ });
1550
+ } else {
1551
+ await this.#db.deleteKV(TABLE_MESSAGES, orderKey);
1552
+ }
1553
+ }
1554
+ }
1555
+ for (const threadId of threadIds) {
1556
+ const thread = await this.getThreadById({ threadId });
1557
+ if (thread) {
1558
+ const updatedThread = {
1559
+ ...thread,
1560
+ updatedAt: /* @__PURE__ */ new Date()
1561
+ };
1562
+ await this.#db.putKV({
1563
+ tableName: TABLE_THREADS,
1564
+ key: this.#db.getKey(TABLE_THREADS, { id: threadId }),
1565
+ value: updatedThread
1566
+ });
1567
+ }
1568
+ }
1507
1569
  } catch (error) {
1508
- this.logger.error(`Failed to list KV for ${tableName}:`, error);
1509
- throw new Error(`Failed to list KV: ${error.message}`);
1570
+ throw new MastraError(
1571
+ {
1572
+ id: createStorageErrorId("CLOUDFLARE", "DELETE_MESSAGES", "FAILED"),
1573
+ domain: ErrorDomain.STORAGE,
1574
+ category: ErrorCategory.THIRD_PARTY,
1575
+ details: { messageIds: JSON.stringify(messageIds) }
1576
+ },
1577
+ error
1578
+ );
1510
1579
  }
1511
1580
  }
1512
1581
  };
@@ -1514,14 +1583,19 @@ function transformScoreRow(row) {
1514
1583
  return transformScoreRow$1(row);
1515
1584
  }
1516
1585
  var ScoresStorageCloudflare = class extends ScoresStorage {
1517
- operations;
1518
- constructor({ operations }) {
1586
+ #db;
1587
+ constructor(config) {
1519
1588
  super();
1520
- this.operations = operations;
1589
+ this.#db = new CloudflareKVDB(resolveCloudflareConfig(config));
1590
+ }
1591
+ async init() {
1592
+ }
1593
+ async dangerouslyClearAll() {
1594
+ await this.#db.clearTable({ tableName: TABLE_SCORERS });
1521
1595
  }
1522
1596
  async getScoreById({ id }) {
1523
1597
  try {
1524
- const score = await this.operations.getKV(TABLE_SCORERS, id);
1598
+ const score = await this.#db.getKV(TABLE_SCORERS, id);
1525
1599
  if (!score) {
1526
1600
  return null;
1527
1601
  }
@@ -1536,8 +1610,8 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1536
1610
  },
1537
1611
  error
1538
1612
  );
1539
- this.logger.trackException(mastraError);
1540
- this.logger.error(mastraError.toString());
1613
+ this.logger?.trackException(mastraError);
1614
+ this.logger?.error(mastraError.toString());
1541
1615
  return null;
1542
1616
  }
1543
1617
  }
@@ -1552,7 +1626,7 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1552
1626
  domain: ErrorDomain.STORAGE,
1553
1627
  category: ErrorCategory.USER,
1554
1628
  details: {
1555
- scorer: score.scorer?.id ?? "unknown",
1629
+ scorer: typeof score.scorer?.id === "string" ? score.scorer.id : String(score.scorer?.id ?? "unknown"),
1556
1630
  entityId: score.entityId ?? "unknown",
1557
1631
  entityType: score.entityType ?? "unknown",
1558
1632
  traceId: score.traceId ?? "",
@@ -1580,7 +1654,7 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1580
1654
  serializedRecord.id = id;
1581
1655
  serializedRecord.createdAt = now.toISOString();
1582
1656
  serializedRecord.updatedAt = now.toISOString();
1583
- await this.operations.putKV({
1657
+ await this.#db.putKV({
1584
1658
  tableName: TABLE_SCORERS,
1585
1659
  key: id,
1586
1660
  value: serializedRecord
@@ -1596,8 +1670,8 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1596
1670
  },
1597
1671
  error
1598
1672
  );
1599
- this.logger.trackException(mastraError);
1600
- this.logger.error(mastraError.toString());
1673
+ this.logger?.trackException(mastraError);
1674
+ this.logger?.error(mastraError.toString());
1601
1675
  throw mastraError;
1602
1676
  }
1603
1677
  }
@@ -1609,10 +1683,10 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1609
1683
  pagination
1610
1684
  }) {
1611
1685
  try {
1612
- const keys = await this.operations.listKV(TABLE_SCORERS);
1686
+ const keys = await this.#db.listKV(TABLE_SCORERS);
1613
1687
  const scores = [];
1614
1688
  for (const { name: key } of keys) {
1615
- const score = await this.operations.getKV(TABLE_SCORERS, key);
1689
+ const score = await this.#db.getKV(TABLE_SCORERS, key);
1616
1690
  if (entityId && score.entityId !== entityId) {
1617
1691
  continue;
1618
1692
  }
@@ -1666,10 +1740,10 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1666
1740
  pagination
1667
1741
  }) {
1668
1742
  try {
1669
- const keys = await this.operations.listKV(TABLE_SCORERS);
1743
+ const keys = await this.#db.listKV(TABLE_SCORERS);
1670
1744
  const scores = [];
1671
1745
  for (const { name: key } of keys) {
1672
- const score = await this.operations.getKV(TABLE_SCORERS, key);
1746
+ const score = await this.#db.getKV(TABLE_SCORERS, key);
1673
1747
  if (score && score.runId === runId) {
1674
1748
  scores.push(transformScoreRow(score));
1675
1749
  }
@@ -1704,8 +1778,8 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1704
1778
  },
1705
1779
  error
1706
1780
  );
1707
- this.logger.trackException(mastraError);
1708
- this.logger.error(mastraError.toString());
1781
+ this.logger?.trackException(mastraError);
1782
+ this.logger?.error(mastraError.toString());
1709
1783
  return { pagination: { total: 0, page: 0, perPage: 100, hasMore: false }, scores: [] };
1710
1784
  }
1711
1785
  }
@@ -1715,10 +1789,10 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1715
1789
  pagination
1716
1790
  }) {
1717
1791
  try {
1718
- const keys = await this.operations.listKV(TABLE_SCORERS);
1792
+ const keys = await this.#db.listKV(TABLE_SCORERS);
1719
1793
  const scores = [];
1720
1794
  for (const { name: key } of keys) {
1721
- const score = await this.operations.getKV(TABLE_SCORERS, key);
1795
+ const score = await this.#db.getKV(TABLE_SCORERS, key);
1722
1796
  if (score && score.entityId === entityId && score.entityType === entityType) {
1723
1797
  scores.push(transformScoreRow(score));
1724
1798
  }
@@ -1753,8 +1827,8 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1753
1827
  },
1754
1828
  error
1755
1829
  );
1756
- this.logger.trackException(mastraError);
1757
- this.logger.error(mastraError.toString());
1830
+ this.logger?.trackException(mastraError);
1831
+ this.logger?.error(mastraError.toString());
1758
1832
  return { pagination: { total: 0, page: 0, perPage: 100, hasMore: false }, scores: [] };
1759
1833
  }
1760
1834
  }
@@ -1764,10 +1838,10 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1764
1838
  pagination
1765
1839
  }) {
1766
1840
  try {
1767
- const keys = await this.operations.listKV(TABLE_SCORERS);
1841
+ const keys = await this.#db.listKV(TABLE_SCORERS);
1768
1842
  const scores = [];
1769
1843
  for (const { name: key } of keys) {
1770
- const score = await this.operations.getKV(TABLE_SCORERS, key);
1844
+ const score = await this.#db.getKV(TABLE_SCORERS, key);
1771
1845
  if (score && score.traceId === traceId && score.spanId === spanId) {
1772
1846
  scores.push(transformScoreRow(score));
1773
1847
  }
@@ -1809,10 +1883,15 @@ var ScoresStorageCloudflare = class extends ScoresStorage {
1809
1883
  }
1810
1884
  };
1811
1885
  var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
1812
- operations;
1813
- constructor({ operations }) {
1886
+ #db;
1887
+ constructor(config) {
1814
1888
  super();
1815
- this.operations = operations;
1889
+ this.#db = new CloudflareKVDB(resolveCloudflareConfig(config));
1890
+ }
1891
+ async init() {
1892
+ }
1893
+ async dangerouslyClearAll() {
1894
+ await this.#db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
1816
1895
  }
1817
1896
  validateWorkflowParams(params) {
1818
1897
  const { workflowName, runId } = params;
@@ -1838,17 +1917,20 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
1838
1917
  }
1839
1918
  async persistWorkflowSnapshot(params) {
1840
1919
  try {
1841
- const { workflowName, runId, resourceId, snapshot } = params;
1842
- await this.operations.putKV({
1920
+ const { workflowName, runId, resourceId, snapshot, createdAt, updatedAt } = params;
1921
+ const now = /* @__PURE__ */ new Date();
1922
+ const existingKey = this.#db.getKey(TABLE_WORKFLOW_SNAPSHOT, { workflow_name: workflowName, run_id: runId });
1923
+ const existing = await this.#db.getKV(TABLE_WORKFLOW_SNAPSHOT, existingKey);
1924
+ await this.#db.putKV({
1843
1925
  tableName: TABLE_WORKFLOW_SNAPSHOT,
1844
- key: this.operations.getKey(TABLE_WORKFLOW_SNAPSHOT, { workflow_name: workflowName, run_id: runId }),
1926
+ key: existingKey,
1845
1927
  value: {
1846
1928
  workflow_name: workflowName,
1847
1929
  run_id: runId,
1848
1930
  resourceId,
1849
1931
  snapshot: JSON.stringify(snapshot),
1850
- createdAt: /* @__PURE__ */ new Date(),
1851
- updatedAt: /* @__PURE__ */ new Date()
1932
+ createdAt: existing?.createdAt ?? createdAt ?? now,
1933
+ updatedAt: updatedAt ?? now
1852
1934
  }
1853
1935
  });
1854
1936
  } catch (error) {
@@ -1871,8 +1953,8 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
1871
1953
  try {
1872
1954
  this.validateWorkflowParams(params);
1873
1955
  const { workflowName, runId } = params;
1874
- const key = this.operations.getKey(TABLE_WORKFLOW_SNAPSHOT, { workflow_name: workflowName, run_id: runId });
1875
- const data = await this.operations.getKV(TABLE_WORKFLOW_SNAPSHOT, key);
1956
+ const key = this.#db.getKey(TABLE_WORKFLOW_SNAPSHOT, { workflow_name: workflowName, run_id: runId });
1957
+ const data = await this.#db.getKV(TABLE_WORKFLOW_SNAPSHOT, key);
1876
1958
  if (!data) return null;
1877
1959
  const snapshotData = typeof data.snapshot === "string" ? JSON.parse(data.snapshot) : data.snapshot;
1878
1960
  return snapshotData;
@@ -1918,7 +2000,7 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
1918
2000
  runId,
1919
2001
  resourceId
1920
2002
  }) {
1921
- const prefix = this.operations.namespacePrefix ? `${this.operations.namespacePrefix}:` : "";
2003
+ const prefix = this.#db.namespacePrefix ? `${this.#db.namespacePrefix}:` : "";
1922
2004
  let key = `${prefix}${TABLE_WORKFLOW_SNAPSHOT}`;
1923
2005
  if (workflowName) key += `:${workflowName}`;
1924
2006
  if (runId) key += `:${runId}`;
@@ -1949,7 +2031,7 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
1949
2031
  const normalizedPerPage = normalizePerPage(perPage, 20);
1950
2032
  const offset = page * normalizedPerPage;
1951
2033
  const prefix = this.buildWorkflowSnapshotPrefix({ workflowName });
1952
- const keyObjs = await this.operations.listKV(TABLE_WORKFLOW_SNAPSHOT, { prefix });
2034
+ const keyObjs = await this.#db.listKV(TABLE_WORKFLOW_SNAPSHOT, { prefix });
1953
2035
  const runs = [];
1954
2036
  for (const { name: key } of keyObjs) {
1955
2037
  const parts = key.split(":");
@@ -1958,21 +2040,20 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
1958
2040
  const wfName = parts[idx + 1];
1959
2041
  const keyResourceId = parts.length > idx + 3 ? parts[idx + 3] : void 0;
1960
2042
  if (workflowName && wfName !== workflowName) continue;
1961
- if (resourceId && keyResourceId !== resourceId) continue;
1962
- const data = await this.operations.getKV(TABLE_WORKFLOW_SNAPSHOT, key);
2043
+ const data = await this.#db.getKV(TABLE_WORKFLOW_SNAPSHOT, key);
1963
2044
  if (!data) continue;
1964
2045
  try {
1965
- if (resourceId && !keyResourceId) continue;
2046
+ const effectiveResourceId = keyResourceId || data.resourceId;
2047
+ if (resourceId && effectiveResourceId !== resourceId) continue;
1966
2048
  const snapshotData = typeof data.snapshot === "string" ? JSON.parse(data.snapshot) : data.snapshot;
1967
2049
  if (status && snapshotData.status !== status) continue;
1968
2050
  const createdAt = ensureDate(data.createdAt);
1969
2051
  if (fromDate && createdAt && createdAt < fromDate) continue;
1970
2052
  if (toDate && createdAt && createdAt > toDate) continue;
1971
- const resourceIdToUse = keyResourceId || data.resourceId;
1972
2053
  const run = this.parseWorkflowRun({
1973
2054
  ...data,
1974
2055
  workflow_name: wfName,
1975
- resourceId: resourceIdToUse,
2056
+ resourceId: effectiveResourceId,
1976
2057
  snapshot: snapshotData
1977
2058
  });
1978
2059
  runs.push(run);
@@ -2013,7 +2094,7 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
2013
2094
  throw new Error("runId, workflowName, are required");
2014
2095
  }
2015
2096
  const prefix = this.buildWorkflowSnapshotPrefix({ workflowName, runId });
2016
- const keyObjs = await this.operations.listKV(TABLE_WORKFLOW_SNAPSHOT, { prefix });
2097
+ const keyObjs = await this.#db.listKV(TABLE_WORKFLOW_SNAPSHOT, { prefix });
2017
2098
  if (!keyObjs.length) return null;
2018
2099
  const exactKey = keyObjs.find((k) => {
2019
2100
  const parts = k.name.split(":");
@@ -2024,7 +2105,7 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
2024
2105
  return wfName === workflowName && rId === runId;
2025
2106
  });
2026
2107
  if (!exactKey) return null;
2027
- const data = await this.operations.getKV(TABLE_WORKFLOW_SNAPSHOT, exactKey.name);
2108
+ const data = await this.#db.getKV(TABLE_WORKFLOW_SNAPSHOT, exactKey.name);
2028
2109
  if (!data) return null;
2029
2110
  const snapshotData = typeof data.snapshot === "string" ? JSON.parse(data.snapshot) : data.snapshot;
2030
2111
  return this.parseWorkflowRun({ ...data, snapshot: snapshotData });
@@ -2051,8 +2132,8 @@ var WorkflowsStorageCloudflare = class extends WorkflowsStorage {
2051
2132
  if (!runId || !workflowName) {
2052
2133
  throw new Error("runId and workflowName are required");
2053
2134
  }
2054
- const key = this.operations.getKey(TABLE_WORKFLOW_SNAPSHOT, { workflow_name: workflowName, run_id: runId });
2055
- await this.operations.deleteKV(TABLE_WORKFLOW_SNAPSHOT, key);
2135
+ const key = this.#db.getKey(TABLE_WORKFLOW_SNAPSHOT, { workflow_name: workflowName, run_id: runId });
2136
+ await this.#db.deleteKV(TABLE_WORKFLOW_SNAPSHOT, key);
2056
2137
  } catch (error) {
2057
2138
  throw new MastraError(
2058
2139
  {
@@ -2108,20 +2189,36 @@ var CloudflareStore = class extends MastraStorage {
2108
2189
  }
2109
2190
  }
2110
2191
  get supports() {
2111
- const supports = super.supports;
2112
- supports.listScoresBySpan = true;
2113
- supports.resourceWorkingMemory = true;
2114
- supports.selectByIncludeResourceScope = true;
2115
- return supports;
2192
+ return {
2193
+ selectByIncludeResourceScope: true,
2194
+ resourceWorkingMemory: true,
2195
+ hasColumn: false,
2196
+ createTable: false,
2197
+ deleteMessages: true,
2198
+ observability: false,
2199
+ indexManagement: false,
2200
+ listScoresBySpan: true,
2201
+ agents: false
2202
+ };
2116
2203
  }
2117
2204
  constructor(config) {
2118
2205
  super({ id: config.id, name: "Cloudflare", disableInit: config.disableInit });
2119
2206
  try {
2207
+ let workflows;
2208
+ let memory;
2209
+ let scores;
2120
2210
  if (isWorkersConfig(config)) {
2121
2211
  this.validateWorkersConfig(config);
2122
2212
  this.bindings = config.bindings;
2123
2213
  this.namespacePrefix = config.keyPrefix?.trim() || "";
2124
2214
  this.logger.info("Using Cloudflare KV Workers Binding API");
2215
+ const domainConfig = {
2216
+ bindings: this.bindings,
2217
+ keyPrefix: this.namespacePrefix
2218
+ };
2219
+ workflows = new WorkflowsStorageCloudflare(domainConfig);
2220
+ memory = new MemoryStorageCloudflare(domainConfig);
2221
+ scores = new ScoresStorageCloudflare(domainConfig);
2125
2222
  } else {
2126
2223
  this.validateRestConfig(config);
2127
2224
  this.accountId = config.accountId.trim();
@@ -2130,24 +2227,16 @@ var CloudflareStore = class extends MastraStorage {
2130
2227
  apiToken: config.apiToken.trim()
2131
2228
  });
2132
2229
  this.logger.info("Using Cloudflare KV REST API");
2230
+ const domainConfig = {
2231
+ client: this.client,
2232
+ accountId: this.accountId,
2233
+ namespacePrefix: this.namespacePrefix
2234
+ };
2235
+ workflows = new WorkflowsStorageCloudflare(domainConfig);
2236
+ memory = new MemoryStorageCloudflare(domainConfig);
2237
+ scores = new ScoresStorageCloudflare(domainConfig);
2133
2238
  }
2134
- const operations = new StoreOperationsCloudflare({
2135
- accountId: this.accountId,
2136
- client: this.client,
2137
- namespacePrefix: this.namespacePrefix,
2138
- bindings: this.bindings
2139
- });
2140
- const workflows = new WorkflowsStorageCloudflare({
2141
- operations
2142
- });
2143
- const memory = new MemoryStorageCloudflare({
2144
- operations
2145
- });
2146
- const scores = new ScoresStorageCloudflare({
2147
- operations
2148
- });
2149
2239
  this.stores = {
2150
- operations,
2151
2240
  workflows,
2152
2241
  memory,
2153
2242
  scores
@@ -2163,156 +2252,6 @@ var CloudflareStore = class extends MastraStorage {
2163
2252
  );
2164
2253
  }
2165
2254
  }
2166
- async createTable({
2167
- tableName,
2168
- schema
2169
- }) {
2170
- return this.stores.operations.createTable({ tableName, schema });
2171
- }
2172
- async alterTable(_args) {
2173
- return this.stores.operations.alterTable(_args);
2174
- }
2175
- async clearTable({ tableName }) {
2176
- return this.stores.operations.clearTable({ tableName });
2177
- }
2178
- async dropTable({ tableName }) {
2179
- return this.stores.operations.dropTable({ tableName });
2180
- }
2181
- async insert({
2182
- tableName,
2183
- record
2184
- }) {
2185
- return this.stores.operations.insert({ tableName, record });
2186
- }
2187
- async load({ tableName, keys }) {
2188
- return this.stores.operations.load({ tableName, keys });
2189
- }
2190
- async getThreadById({ threadId }) {
2191
- return this.stores.memory.getThreadById({ threadId });
2192
- }
2193
- async saveThread({ thread }) {
2194
- return this.stores.memory.saveThread({ thread });
2195
- }
2196
- async updateThread({
2197
- id,
2198
- title,
2199
- metadata
2200
- }) {
2201
- return this.stores.memory.updateThread({ id, title, metadata });
2202
- }
2203
- async deleteThread({ threadId }) {
2204
- return this.stores.memory.deleteThread({ threadId });
2205
- }
2206
- async saveMessages(args) {
2207
- return this.stores.memory.saveMessages(args);
2208
- }
2209
- async updateWorkflowResults({
2210
- workflowName,
2211
- runId,
2212
- stepId,
2213
- result,
2214
- requestContext
2215
- }) {
2216
- return this.stores.workflows.updateWorkflowResults({ workflowName, runId, stepId, result, requestContext });
2217
- }
2218
- async updateWorkflowState({
2219
- workflowName,
2220
- runId,
2221
- opts
2222
- }) {
2223
- return this.stores.workflows.updateWorkflowState({ workflowName, runId, opts });
2224
- }
2225
- async listMessagesById({ messageIds }) {
2226
- return this.stores.memory.listMessagesById({ messageIds });
2227
- }
2228
- async persistWorkflowSnapshot(params) {
2229
- return this.stores.workflows.persistWorkflowSnapshot(params);
2230
- }
2231
- async loadWorkflowSnapshot(params) {
2232
- return this.stores.workflows.loadWorkflowSnapshot(params);
2233
- }
2234
- async batchInsert(input) {
2235
- return this.stores.operations.batchInsert(input);
2236
- }
2237
- async listWorkflowRuns({
2238
- workflowName,
2239
- perPage = 20,
2240
- page = 0,
2241
- resourceId,
2242
- fromDate,
2243
- toDate,
2244
- status
2245
- } = {}) {
2246
- return this.stores.workflows.listWorkflowRuns({
2247
- workflowName,
2248
- perPage,
2249
- page,
2250
- resourceId,
2251
- fromDate,
2252
- toDate,
2253
- status
2254
- });
2255
- }
2256
- async getWorkflowRunById({
2257
- runId,
2258
- workflowName
2259
- }) {
2260
- return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
2261
- }
2262
- async deleteWorkflowRunById({ runId, workflowName }) {
2263
- return this.stores.workflows.deleteWorkflowRunById({ runId, workflowName });
2264
- }
2265
- async updateMessages(args) {
2266
- return this.stores.memory.updateMessages(args);
2267
- }
2268
- async getScoreById({ id }) {
2269
- return this.stores.scores.getScoreById({ id });
2270
- }
2271
- async saveScore(score) {
2272
- return this.stores.scores.saveScore(score);
2273
- }
2274
- async listScoresByRunId({
2275
- runId,
2276
- pagination
2277
- }) {
2278
- return this.stores.scores.listScoresByRunId({ runId, pagination });
2279
- }
2280
- async listScoresByEntityId({
2281
- entityId,
2282
- entityType,
2283
- pagination
2284
- }) {
2285
- return this.stores.scores.listScoresByEntityId({ entityId, entityType, pagination });
2286
- }
2287
- async listScoresByScorerId({
2288
- scorerId,
2289
- entityId,
2290
- entityType,
2291
- source,
2292
- pagination
2293
- }) {
2294
- return this.stores.scores.listScoresByScorerId({ scorerId, entityId, entityType, source, pagination });
2295
- }
2296
- async listScoresBySpan({
2297
- traceId,
2298
- spanId,
2299
- pagination
2300
- }) {
2301
- return this.stores.scores.listScoresBySpan({ traceId, spanId, pagination });
2302
- }
2303
- async getResourceById({ resourceId }) {
2304
- return this.stores.memory.getResourceById({ resourceId });
2305
- }
2306
- async saveResource({ resource }) {
2307
- return this.stores.memory.saveResource({ resource });
2308
- }
2309
- async updateResource({
2310
- resourceId,
2311
- workingMemory,
2312
- metadata
2313
- }) {
2314
- return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
2315
- }
2316
2255
  async close() {
2317
2256
  }
2318
2257
  };