@mastra/libsql 0.11.0 → 0.11.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ import type { InValue } from '@libsql/client';
2
+ import type { IMastraLogger } from '@mastra/core/logger';
3
+ import type { TABLE_NAMES } from '@mastra/core/storage';
4
+ import { parseSqlIdentifier } from '@mastra/core/utils';
5
+
6
+ export function createExecuteWriteOperationWithRetry({
7
+ logger,
8
+ maxRetries,
9
+ initialBackoffMs,
10
+ }: {
11
+ logger: IMastraLogger;
12
+ maxRetries: number;
13
+ initialBackoffMs: number;
14
+ }) {
15
+ return async function executeWriteOperationWithRetry<T>(
16
+ operationFn: () => Promise<T>,
17
+ operationDescription: string,
18
+ ): Promise<T> {
19
+ let retries = 0;
20
+
21
+ while (true) {
22
+ try {
23
+ return await operationFn();
24
+ } catch (error: any) {
25
+ if (
26
+ error.message &&
27
+ (error.message.includes('SQLITE_BUSY') || error.message.includes('database is locked')) &&
28
+ retries < maxRetries
29
+ ) {
30
+ retries++;
31
+ const backoffTime = initialBackoffMs * Math.pow(2, retries - 1);
32
+ logger.warn(
33
+ `LibSQLStore: Encountered SQLITE_BUSY during ${operationDescription}. Retrying (${retries}/${maxRetries}) in ${backoffTime}ms...`,
34
+ );
35
+ await new Promise(resolve => setTimeout(resolve, backoffTime));
36
+ } else {
37
+ logger.error(`LibSQLStore: Error during ${operationDescription} after ${retries} retries: ${error}`);
38
+ throw error;
39
+ }
40
+ }
41
+ }
42
+ };
43
+ }
44
+
45
+ export function prepareStatement({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): {
46
+ sql: string;
47
+ args: InValue[];
48
+ } {
49
+ const parsedTableName = parseSqlIdentifier(tableName, 'table name');
50
+ const columns = Object.keys(record).map(col => parseSqlIdentifier(col, 'column name'));
51
+ const values = Object.values(record).map(v => {
52
+ if (typeof v === `undefined`) {
53
+ // returning an undefined value will cause libsql to throw
54
+ return null;
55
+ }
56
+ if (v instanceof Date) {
57
+ return v.toISOString();
58
+ }
59
+ return typeof v === 'object' ? JSON.stringify(v) : v;
60
+ });
61
+ const placeholders = values.map(() => '?').join(', ');
62
+
63
+ return {
64
+ sql: `INSERT OR REPLACE INTO ${parsedTableName} (${columns.join(', ')}) VALUES (${placeholders})`,
65
+ args: values,
66
+ };
67
+ }
@@ -0,0 +1,198 @@
1
+ import type { Client, InValue } from '@libsql/client';
2
+ import type { WorkflowRun, WorkflowRuns, WorkflowRunState } from '@mastra/core';
3
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
4
+ import { TABLE_WORKFLOW_SNAPSHOT, WorkflowsStorage } from '@mastra/core/storage';
5
+ import type { StoreOperationsLibSQL } from '../operations';
6
+
7
+ function parseWorkflowRun(row: Record<string, any>): WorkflowRun {
8
+ let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
9
+ if (typeof parsedSnapshot === 'string') {
10
+ try {
11
+ parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
12
+ } catch (e) {
13
+ // If parsing fails, return the raw snapshot string
14
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
15
+ }
16
+ }
17
+ return {
18
+ workflowName: row.workflow_name as string,
19
+ runId: row.run_id as string,
20
+ snapshot: parsedSnapshot,
21
+ resourceId: row.resourceId as string,
22
+ createdAt: new Date(row.createdAt as string),
23
+ updatedAt: new Date(row.updatedAt as string),
24
+ };
25
+ }
26
+
27
+ export class WorkflowsLibSQL extends WorkflowsStorage {
28
+ operations: StoreOperationsLibSQL;
29
+ client: Client;
30
+ constructor({ operations, client }: { operations: StoreOperationsLibSQL; client: Client }) {
31
+ super();
32
+ this.operations = operations;
33
+ this.client = client;
34
+ }
35
+
36
+ async persistWorkflowSnapshot({
37
+ workflowName,
38
+ runId,
39
+ snapshot,
40
+ }: {
41
+ workflowName: string;
42
+ runId: string;
43
+ snapshot: WorkflowRunState;
44
+ }) {
45
+ const data = {
46
+ workflow_name: workflowName,
47
+ run_id: runId,
48
+ snapshot,
49
+ createdAt: new Date(),
50
+ updatedAt: new Date(),
51
+ };
52
+
53
+ this.logger.debug('Persisting workflow snapshot', { workflowName, runId, data });
54
+ await this.operations.insert({
55
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
56
+ record: data,
57
+ });
58
+ }
59
+
60
+ async loadWorkflowSnapshot({
61
+ workflowName,
62
+ runId,
63
+ }: {
64
+ workflowName: string;
65
+ runId: string;
66
+ }): Promise<WorkflowRunState | null> {
67
+ this.logger.debug('Loading workflow snapshot', { workflowName, runId });
68
+ const d = await this.operations.load<{ snapshot: WorkflowRunState }>({
69
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
70
+ keys: { workflow_name: workflowName, run_id: runId },
71
+ });
72
+
73
+ return d ? d.snapshot : null;
74
+ }
75
+
76
+ async getWorkflowRunById({
77
+ runId,
78
+ workflowName,
79
+ }: {
80
+ runId: string;
81
+ workflowName?: string;
82
+ }): Promise<WorkflowRun | null> {
83
+ const conditions: string[] = [];
84
+ const args: (string | number)[] = [];
85
+
86
+ if (runId) {
87
+ conditions.push('run_id = ?');
88
+ args.push(runId);
89
+ }
90
+
91
+ if (workflowName) {
92
+ conditions.push('workflow_name = ?');
93
+ args.push(workflowName);
94
+ }
95
+
96
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
97
+
98
+ try {
99
+ const result = await this.client.execute({
100
+ sql: `SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause}`,
101
+ args,
102
+ });
103
+
104
+ if (!result.rows?.[0]) {
105
+ return null;
106
+ }
107
+
108
+ return parseWorkflowRun(result.rows[0]);
109
+ } catch (error) {
110
+ throw new MastraError(
111
+ {
112
+ id: 'LIBSQL_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED',
113
+ domain: ErrorDomain.STORAGE,
114
+ category: ErrorCategory.THIRD_PARTY,
115
+ },
116
+ error,
117
+ );
118
+ }
119
+ }
120
+
121
+ async getWorkflowRuns({
122
+ workflowName,
123
+ fromDate,
124
+ toDate,
125
+ limit,
126
+ offset,
127
+ resourceId,
128
+ }: {
129
+ workflowName?: string;
130
+ fromDate?: Date;
131
+ toDate?: Date;
132
+ limit?: number;
133
+ offset?: number;
134
+ resourceId?: string;
135
+ } = {}): Promise<WorkflowRuns> {
136
+ try {
137
+ const conditions: string[] = [];
138
+ const args: InValue[] = [];
139
+
140
+ if (workflowName) {
141
+ conditions.push('workflow_name = ?');
142
+ args.push(workflowName);
143
+ }
144
+
145
+ if (fromDate) {
146
+ conditions.push('createdAt >= ?');
147
+ args.push(fromDate.toISOString());
148
+ }
149
+
150
+ if (toDate) {
151
+ conditions.push('createdAt <= ?');
152
+ args.push(toDate.toISOString());
153
+ }
154
+
155
+ if (resourceId) {
156
+ const hasResourceId = await this.operations.hasColumn(TABLE_WORKFLOW_SNAPSHOT, 'resourceId');
157
+ if (hasResourceId) {
158
+ conditions.push('resourceId = ?');
159
+ args.push(resourceId);
160
+ } else {
161
+ console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
162
+ }
163
+ }
164
+
165
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
166
+
167
+ let total = 0;
168
+ // Only get total count when using pagination
169
+ if (limit !== undefined && offset !== undefined) {
170
+ const countResult = await this.client.execute({
171
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause}`,
172
+ args,
173
+ });
174
+ total = Number(countResult.rows?.[0]?.count ?? 0);
175
+ }
176
+
177
+ // Get results
178
+ const result = await this.client.execute({
179
+ sql: `SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC${limit !== undefined && offset !== undefined ? ` LIMIT ? OFFSET ?` : ''}`,
180
+ args: limit !== undefined && offset !== undefined ? [...args, limit, offset] : args,
181
+ });
182
+
183
+ const runs = (result.rows || []).map(row => parseWorkflowRun(row));
184
+
185
+ // Use runs.length as total when not paginating
186
+ return { runs, total: total || runs.length };
187
+ } catch (error) {
188
+ throw new MastraError(
189
+ {
190
+ id: 'LIBSQL_STORE_GET_WORKFLOW_RUNS_FAILED',
191
+ domain: ErrorDomain.STORAGE,
192
+ category: ErrorCategory.THIRD_PARTY,
193
+ },
194
+ error,
195
+ );
196
+ }
197
+ }
198
+ }