@mastra/libsql 0.13.8-alpha.0 → 0.13.8-alpha.2

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.
@@ -1,421 +0,0 @@
1
- import type { Client, InValue } from '@libsql/client';
2
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
- import type { WorkflowRun, WorkflowRuns } from '@mastra/core/storage';
4
- import { TABLE_WORKFLOW_SNAPSHOT, WorkflowsStorage } from '@mastra/core/storage';
5
- import type { WorkflowRunState, StepResult } from '@mastra/core/workflows';
6
- import type { StoreOperationsLibSQL } from '../operations';
7
-
8
- function parseWorkflowRun(row: Record<string, any>): WorkflowRun {
9
- let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
10
- if (typeof parsedSnapshot === 'string') {
11
- try {
12
- parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
13
- } catch (e) {
14
- // If parsing fails, return the raw snapshot string
15
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
16
- }
17
- }
18
- return {
19
- workflowName: row.workflow_name as string,
20
- runId: row.run_id as string,
21
- snapshot: parsedSnapshot,
22
- resourceId: row.resourceId as string,
23
- createdAt: new Date(row.createdAt as string),
24
- updatedAt: new Date(row.updatedAt as string),
25
- };
26
- }
27
-
28
- export class WorkflowsLibSQL extends WorkflowsStorage {
29
- operations: StoreOperationsLibSQL;
30
- client: Client;
31
- private readonly maxRetries: number;
32
- private readonly initialBackoffMs: number;
33
-
34
- constructor({
35
- operations,
36
- client,
37
- maxRetries = 5,
38
- initialBackoffMs = 500,
39
- }: {
40
- operations: StoreOperationsLibSQL;
41
- client: Client;
42
- maxRetries?: number;
43
- initialBackoffMs?: number;
44
- }) {
45
- super();
46
- this.operations = operations;
47
- this.client = client;
48
- this.maxRetries = maxRetries;
49
- this.initialBackoffMs = initialBackoffMs;
50
-
51
- // Set PRAGMA settings to help with database locks
52
- // Note: This is async but we can't await in constructor, so we'll handle it as a fire-and-forget
53
- this.setupPragmaSettings().catch(err =>
54
- this.logger.warn('LibSQL Workflows: Failed to setup PRAGMA settings.', err),
55
- );
56
- }
57
-
58
- private async setupPragmaSettings() {
59
- try {
60
- // Set busy timeout to wait longer before returning busy errors
61
- await this.client.execute('PRAGMA busy_timeout = 10000;');
62
- this.logger.debug('LibSQL Workflows: PRAGMA busy_timeout=10000 set.');
63
-
64
- // Enable WAL mode for better concurrency (if supported)
65
- try {
66
- await this.client.execute('PRAGMA journal_mode = WAL;');
67
- this.logger.debug('LibSQL Workflows: PRAGMA journal_mode=WAL set.');
68
- } catch {
69
- this.logger.debug('LibSQL Workflows: WAL mode not supported, using default journal mode.');
70
- }
71
-
72
- // Set synchronous mode for better durability vs performance trade-off
73
- try {
74
- await this.client.execute('PRAGMA synchronous = NORMAL;');
75
- this.logger.debug('LibSQL Workflows: PRAGMA synchronous=NORMAL set.');
76
- } catch {
77
- this.logger.debug('LibSQL Workflows: Failed to set synchronous mode.');
78
- }
79
- } catch (err) {
80
- this.logger.warn('LibSQL Workflows: Failed to set PRAGMA settings.', err);
81
- }
82
- }
83
-
84
- private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
85
- let attempts = 0;
86
- let backoff = this.initialBackoffMs;
87
-
88
- while (attempts < this.maxRetries) {
89
- try {
90
- return await operation();
91
- } catch (error: any) {
92
- // Log the error details for debugging
93
- this.logger.debug('LibSQL Workflows: Error caught in retry loop', {
94
- errorType: error.constructor.name,
95
- errorCode: error.code,
96
- errorMessage: error.message,
97
- attempts,
98
- maxRetries: this.maxRetries,
99
- });
100
-
101
- // Check for various database lock/busy conditions
102
- const isLockError =
103
- error.code === 'SQLITE_BUSY' ||
104
- error.code === 'SQLITE_LOCKED' ||
105
- error.message?.toLowerCase().includes('database is locked') ||
106
- error.message?.toLowerCase().includes('database table is locked') ||
107
- error.message?.toLowerCase().includes('table is locked') ||
108
- (error.constructor.name === 'SqliteError' && error.message?.toLowerCase().includes('locked'));
109
-
110
- if (isLockError) {
111
- attempts++;
112
- if (attempts >= this.maxRetries) {
113
- this.logger.error(
114
- `LibSQL Workflows: Operation failed after ${this.maxRetries} attempts due to database lock: ${error.message}`,
115
- { error, attempts, maxRetries: this.maxRetries },
116
- );
117
- throw error;
118
- }
119
- this.logger.warn(
120
- `LibSQL Workflows: Attempt ${attempts} failed due to database lock. Retrying in ${backoff}ms...`,
121
- { errorMessage: error.message, attempts, backoff, maxRetries: this.maxRetries },
122
- );
123
- await new Promise(resolve => setTimeout(resolve, backoff));
124
- backoff *= 2;
125
- } else {
126
- // Not a lock error, re-throw immediately
127
- this.logger.error('LibSQL Workflows: Non-lock error occurred, not retrying', { error });
128
- throw error;
129
- }
130
- }
131
- }
132
- throw new Error('LibSQL Workflows: Max retries reached, but no error was re-thrown from the loop.');
133
- }
134
-
135
- async updateWorkflowResults({
136
- workflowName,
137
- runId,
138
- stepId,
139
- result,
140
- runtimeContext,
141
- }: {
142
- workflowName: string;
143
- runId: string;
144
- stepId: string;
145
- result: StepResult<any, any, any, any>;
146
- runtimeContext: Record<string, any>;
147
- }): Promise<Record<string, StepResult<any, any, any, any>>> {
148
- return this.executeWithRetry(async () => {
149
- // Use a transaction to ensure atomicity
150
- const tx = await this.client.transaction('write');
151
- try {
152
- // Load existing snapshot within transaction
153
- const existingSnapshotResult = await tx.execute({
154
- sql: `SELECT snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
155
- args: [workflowName, runId],
156
- });
157
-
158
- let snapshot: WorkflowRunState;
159
- if (!existingSnapshotResult.rows?.[0]) {
160
- // Create new snapshot if none exists
161
- snapshot = {
162
- context: {},
163
- activePaths: [],
164
- timestamp: Date.now(),
165
- suspendedPaths: {},
166
- serializedStepGraph: [],
167
- value: {},
168
- waitingPaths: {},
169
- status: 'pending',
170
- runId: runId,
171
- runtimeContext: {},
172
- } as WorkflowRunState;
173
- } else {
174
- // Parse existing snapshot
175
- const existingSnapshot = existingSnapshotResult.rows[0].snapshot;
176
- snapshot = typeof existingSnapshot === 'string' ? JSON.parse(existingSnapshot) : existingSnapshot;
177
- }
178
-
179
- // Merge the new step result and runtime context
180
- snapshot.context[stepId] = result;
181
- snapshot.runtimeContext = { ...snapshot.runtimeContext, ...runtimeContext };
182
-
183
- // Update the snapshot within the same transaction
184
- await tx.execute({
185
- sql: `UPDATE ${TABLE_WORKFLOW_SNAPSHOT} SET snapshot = ? WHERE workflow_name = ? AND run_id = ?`,
186
- args: [JSON.stringify(snapshot), workflowName, runId],
187
- });
188
-
189
- await tx.commit();
190
- return snapshot.context;
191
- } catch (error) {
192
- if (!tx.closed) {
193
- await tx.rollback();
194
- }
195
- throw error;
196
- }
197
- });
198
- }
199
-
200
- async updateWorkflowState({
201
- workflowName,
202
- runId,
203
- opts,
204
- }: {
205
- workflowName: string;
206
- runId: string;
207
- opts: {
208
- status: string;
209
- result?: StepResult<any, any, any, any>;
210
- error?: string;
211
- suspendedPaths?: Record<string, number[]>;
212
- waitingPaths?: Record<string, number[]>;
213
- };
214
- }): Promise<WorkflowRunState | undefined> {
215
- return this.executeWithRetry(async () => {
216
- // Use a transaction to ensure atomicity
217
- const tx = await this.client.transaction('write');
218
- try {
219
- // Load existing snapshot within transaction
220
- const existingSnapshotResult = await tx.execute({
221
- sql: `SELECT snapshot FROM ${TABLE_WORKFLOW_SNAPSHOT} WHERE workflow_name = ? AND run_id = ?`,
222
- args: [workflowName, runId],
223
- });
224
-
225
- if (!existingSnapshotResult.rows?.[0]) {
226
- await tx.rollback();
227
- return undefined;
228
- }
229
-
230
- // Parse existing snapshot
231
- const existingSnapshot = existingSnapshotResult.rows[0].snapshot;
232
- const snapshot = typeof existingSnapshot === 'string' ? JSON.parse(existingSnapshot) : existingSnapshot;
233
-
234
- if (!snapshot || !snapshot?.context) {
235
- await tx.rollback();
236
- throw new Error(`Snapshot not found for runId ${runId}`);
237
- }
238
-
239
- // Merge the new options with the existing snapshot
240
- const updatedSnapshot = { ...snapshot, ...opts };
241
-
242
- // Update the snapshot within the same transaction
243
- await tx.execute({
244
- sql: `UPDATE ${TABLE_WORKFLOW_SNAPSHOT} SET snapshot = ? WHERE workflow_name = ? AND run_id = ?`,
245
- args: [JSON.stringify(updatedSnapshot), workflowName, runId],
246
- });
247
-
248
- await tx.commit();
249
- return updatedSnapshot;
250
- } catch (error) {
251
- if (!tx.closed) {
252
- await tx.rollback();
253
- }
254
- throw error;
255
- }
256
- });
257
- }
258
-
259
- async persistWorkflowSnapshot({
260
- workflowName,
261
- runId,
262
- snapshot,
263
- }: {
264
- workflowName: string;
265
- runId: string;
266
- snapshot: WorkflowRunState;
267
- }) {
268
- const data = {
269
- workflow_name: workflowName,
270
- run_id: runId,
271
- snapshot,
272
- createdAt: new Date(),
273
- updatedAt: new Date(),
274
- };
275
-
276
- this.logger.debug('Persisting workflow snapshot', { workflowName, runId, data });
277
- await this.operations.insert({
278
- tableName: TABLE_WORKFLOW_SNAPSHOT,
279
- record: data,
280
- });
281
- }
282
-
283
- async loadWorkflowSnapshot({
284
- workflowName,
285
- runId,
286
- }: {
287
- workflowName: string;
288
- runId: string;
289
- }): Promise<WorkflowRunState | null> {
290
- this.logger.debug('Loading workflow snapshot', { workflowName, runId });
291
- const d = await this.operations.load<{ snapshot: WorkflowRunState }>({
292
- tableName: TABLE_WORKFLOW_SNAPSHOT,
293
- keys: { workflow_name: workflowName, run_id: runId },
294
- });
295
-
296
- return d ? d.snapshot : null;
297
- }
298
-
299
- async getWorkflowRunById({
300
- runId,
301
- workflowName,
302
- }: {
303
- runId: string;
304
- workflowName?: string;
305
- }): Promise<WorkflowRun | null> {
306
- const conditions: string[] = [];
307
- const args: (string | number)[] = [];
308
-
309
- if (runId) {
310
- conditions.push('run_id = ?');
311
- args.push(runId);
312
- }
313
-
314
- if (workflowName) {
315
- conditions.push('workflow_name = ?');
316
- args.push(workflowName);
317
- }
318
-
319
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
320
-
321
- try {
322
- const result = await this.client.execute({
323
- sql: `SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC LIMIT 1`,
324
- args,
325
- });
326
-
327
- if (!result.rows?.[0]) {
328
- return null;
329
- }
330
-
331
- return parseWorkflowRun(result.rows[0]);
332
- } catch (error) {
333
- throw new MastraError(
334
- {
335
- id: 'LIBSQL_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED',
336
- domain: ErrorDomain.STORAGE,
337
- category: ErrorCategory.THIRD_PARTY,
338
- },
339
- error,
340
- );
341
- }
342
- }
343
-
344
- async getWorkflowRuns({
345
- workflowName,
346
- fromDate,
347
- toDate,
348
- limit,
349
- offset,
350
- resourceId,
351
- }: {
352
- workflowName?: string;
353
- fromDate?: Date;
354
- toDate?: Date;
355
- limit?: number;
356
- offset?: number;
357
- resourceId?: string;
358
- } = {}): Promise<WorkflowRuns> {
359
- try {
360
- const conditions: string[] = [];
361
- const args: InValue[] = [];
362
-
363
- if (workflowName) {
364
- conditions.push('workflow_name = ?');
365
- args.push(workflowName);
366
- }
367
-
368
- if (fromDate) {
369
- conditions.push('createdAt >= ?');
370
- args.push(fromDate.toISOString());
371
- }
372
-
373
- if (toDate) {
374
- conditions.push('createdAt <= ?');
375
- args.push(toDate.toISOString());
376
- }
377
-
378
- if (resourceId) {
379
- const hasResourceId = await this.operations.hasColumn(TABLE_WORKFLOW_SNAPSHOT, 'resourceId');
380
- if (hasResourceId) {
381
- conditions.push('resourceId = ?');
382
- args.push(resourceId);
383
- } else {
384
- console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
385
- }
386
- }
387
-
388
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
389
-
390
- let total = 0;
391
- // Only get total count when using pagination
392
- if (limit !== undefined && offset !== undefined) {
393
- const countResult = await this.client.execute({
394
- sql: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause}`,
395
- args,
396
- });
397
- total = Number(countResult.rows?.[0]?.count ?? 0);
398
- }
399
-
400
- // Get results
401
- const result = await this.client.execute({
402
- sql: `SELECT * FROM ${TABLE_WORKFLOW_SNAPSHOT} ${whereClause} ORDER BY createdAt DESC${limit !== undefined && offset !== undefined ? ` LIMIT ? OFFSET ?` : ''}`,
403
- args: limit !== undefined && offset !== undefined ? [...args, limit, offset] : args,
404
- });
405
-
406
- const runs = (result.rows || []).map(row => parseWorkflowRun(row));
407
-
408
- // Use runs.length as total when not paginating
409
- return { runs, total: total || runs.length };
410
- } catch (error) {
411
- throw new MastraError(
412
- {
413
- id: 'LIBSQL_STORE_GET_WORKFLOW_RUNS_FAILED',
414
- domain: ErrorDomain.STORAGE,
415
- category: ErrorCategory.THIRD_PARTY,
416
- },
417
- error,
418
- );
419
- }
420
- }
421
- }
@@ -1,16 +0,0 @@
1
- import { createTestSuite } from '@internal/storage-test-utils';
2
- import { Mastra } from '@mastra/core/mastra';
3
-
4
- import { LibSQLStore } from './index';
5
-
6
- const TEST_DB_URL = 'file::memory:?cache=shared';
7
-
8
- const libsql = new LibSQLStore({
9
- url: TEST_DB_URL,
10
- });
11
-
12
- const mastra = new Mastra({
13
- storage: libsql,
14
- });
15
-
16
- createTestSuite(mastra.getStorage()!);