@mastra/libsql 1.22.4-alpha.1 → 1.22.5-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,6 +11,84 @@ import { randomUUID } from "crypto";
11
11
  import { MessageList } from "@mastra/core/agent";
12
12
  import { saveScorePayloadSchema } from "@mastra/core/evals";
13
13
  import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
14
+ //#region src/shared/single-connection-client.ts
15
+ /**
16
+ * Whether `@libsql/client` backs this database with exactly one connection.
17
+ *
18
+ * An in-memory database exists only on the connection that opened it, and each
19
+ * embedded-replica connection carries its own sync state, so `@libsql/client`
20
+ * (>= 0.18.0) gives both a pool of one. Any `execute`/`batch` issued while an
21
+ * interactive `transaction()` holds that connection is rejected immediately
22
+ * with `TRANSACTION_ACTIVE` instead of waiting for the transaction to settle.
23
+ */
24
+ function isSingleConnectionDatabase({ url, syncUrl }) {
25
+ return url.includes(":memory:") || Boolean(syncUrl);
26
+ }
27
+ /**
28
+ * Wraps a single-connection client so client calls queue behind open
29
+ * transactions rather than failing with `TRANSACTION_ACTIVE`.
30
+ *
31
+ * `transaction()` takes the gate and releases it when the transaction commits,
32
+ * rolls back, or closes. `execute`, `batch`, `executeMultiple`, and `migrate`
33
+ * wait for the gate to be free before running but do not hold it — the driver
34
+ * executes them synchronously on the connection, so they cannot interleave
35
+ * with each other. Every other member passes through untouched.
36
+ *
37
+ * Callers must not issue client calls from inside their own open transaction
38
+ * (use `tx.execute`); such a call would wait for the transaction it is part of.
39
+ */
40
+ function gateSingleConnectionClient(client) {
41
+ let gate = Promise.resolve();
42
+ const waitForGate = (run) => gate.then(run, run);
43
+ const transaction = async (mode) => {
44
+ let release;
45
+ const held = new Promise((resolve) => {
46
+ release = resolve;
47
+ });
48
+ const previous = gate;
49
+ gate = previous.then(() => held, () => held);
50
+ await previous.then(() => void 0, () => void 0);
51
+ let tx;
52
+ try {
53
+ tx = await client.transaction(mode);
54
+ } catch (error) {
55
+ release();
56
+ throw error;
57
+ }
58
+ return new Proxy(tx, { get(target, prop) {
59
+ if (prop === "commit" || prop === "rollback") return async () => {
60
+ try {
61
+ await target[prop].call(target);
62
+ } finally {
63
+ release();
64
+ }
65
+ };
66
+ if (prop === "close") return () => {
67
+ try {
68
+ target.close();
69
+ } finally {
70
+ release();
71
+ }
72
+ };
73
+ const value = Reflect.get(target, prop);
74
+ return typeof value === "function" ? value.bind(target) : value;
75
+ } });
76
+ };
77
+ return new Proxy(client, { get(target, prop) {
78
+ switch (prop) {
79
+ case "transaction": return transaction;
80
+ case "execute":
81
+ case "batch":
82
+ case "executeMultiple":
83
+ case "migrate": return (...args) => waitForGate(() => target[prop].apply(target, args));
84
+ default: {
85
+ const value = Reflect.get(target, prop);
86
+ return typeof value === "function" ? value.bind(target) : value;
87
+ }
88
+ }
89
+ } });
90
+ }
91
+ //#endregion
14
92
  //#region src/vector/filter.ts
15
93
  /**
16
94
  * Translates MongoDB-style filters to LibSQL compatible filters.
@@ -426,13 +504,17 @@ var LibSQLVector = class extends MastraVector {
426
504
  this.isMemoryDb = url.includes(":memory:");
427
505
  const isLocalDb = (url.startsWith("file:") || this.isMemoryDb) && !syncUrl;
428
506
  const cwd = process.cwd();
429
- this.turso = createClient({
507
+ const client = createClient({
430
508
  url,
431
509
  syncUrl,
432
510
  authToken,
433
511
  syncInterval,
434
512
  ...isLocalDb ? { timeout: 5e3 } : {}
435
513
  });
514
+ this.turso = isSingleConnectionDatabase({
515
+ url,
516
+ syncUrl
517
+ }) ? gateSingleConnectionClient(client) : client;
436
518
  this.maxRetries = maxRetries;
437
519
  this.initialBackoffMs = initialBackoffMs;
438
520
  if (!Number.isInteger(vectorTopKOverFetchMultiplier) || vectorTopKOverFetchMultiplier < 1) throw new Error("vectorTopKOverFetchMultiplier must be a positive integer");
@@ -1279,24 +1361,26 @@ function transformFromSqlRow({ tableName, sqlRow }) {
1279
1361
  /**
1280
1362
  * Per-client write serialization.
1281
1363
  *
1282
- * `@libsql/client` backs a local (`file:`/`:memory:`) database with a single
1283
- * underlying connection. An interactive `client.transaction('write')` issues a
1284
- * `BEGIN` and then yields to the event loop on every `await tx.execute(...)`.
1285
- * Any autocommit write (`client.execute`/`client.batch`) issued on the same
1286
- * client during that window runs on the same connection so it is swept into
1287
- * the still-open transaction and is committed or rolled back with it, instead
1288
- * of as its own statement. Two concurrent interactive transactions collide the
1289
- * same way ("cannot start a transaction within a transaction").
1364
+ * `@libsql/client` >= 0.18.0 pools connections for local `file:` databases, but
1365
+ * SQLite still admits one writer at a time: an interactive
1366
+ * `client.transaction('write')` holds `BEGIN` open across every
1367
+ * `await tx.execute(...)`, and any other write on the same database in that
1368
+ * window contends on the file lock and can fail with `SQLITE_BUSY` once
1369
+ * `busy_timeout` expires.
1290
1370
  *
1291
1371
  * This is dormant under the default engine but the evented engine runs many
1292
1372
  * concurrent workflow snapshot writes per agent run, so a write issued by an
1293
- * unrelated domain (e.g. creating a dataset experiment) can silently vanish.
1373
+ * unrelated domain (e.g. creating a dataset experiment) can fail spuriously.
1294
1374
  *
1295
1375
  * Serializing every write on a given client closes that window: writes — both
1296
1376
  * autocommit statements and full interactive transactions — run one at a time,
1297
1377
  * so none can interleave with an open transaction. Reads are intentionally not
1298
1378
  * gated; WAL readers never observe a partial write and must not queue behind a
1299
1379
  * long-running writer.
1380
+ *
1381
+ * `:memory:` databases and embedded replicas get a single pooled connection
1382
+ * instead; see `shared/single-connection-client.ts`, which gates *all* calls
1383
+ * (reads included) behind open transactions for those clients.
1300
1384
  */
1301
1385
  const clientWriteChains = /* @__PURE__ */ new WeakMap();
1302
1386
  /**
@@ -14221,11 +14305,12 @@ var LibSQLFactoryStorage = class extends FactoryStorage {
14221
14305
  super();
14222
14306
  this.#config = config;
14223
14307
  const isLocalDb = config.url.startsWith("file:") || config.url.includes(":memory:");
14224
- this.#client = createClient({
14308
+ const client = createClient({
14225
14309
  url: config.url,
14226
14310
  ...config.authToken ? { authToken: config.authToken } : {},
14227
14311
  ...isLocalDb ? { timeout: DEFAULT_CONNECTION_TIMEOUT_MS } : {}
14228
14312
  });
14313
+ this.#client = isSingleConnectionDatabase(config) ? gateSingleConnectionClient(client) : client;
14229
14314
  this.ops = new LibSQLFactoryStorageOps(this.#client, this.#schemas, (fn) => withClientWriteLock(this.#client, fn));
14230
14315
  }
14231
14316
  getMastraStorage() {
@@ -14383,15 +14468,15 @@ var LibSQLStore = class extends MastraCompositeStore {
14383
14468
  mmapSize: config.localPragmas?.mmapSize ?? DEFAULT_LOCAL_MMAP_SIZE
14384
14469
  };
14385
14470
  if ("url" in config) {
14386
- if (config.url.includes(":memory:")) this.shouldCacheInit = false;
14387
14471
  this.isLocalDb = (config.url.startsWith("file:") || config.url.includes(":memory:")) && !config.syncUrl;
14388
- this.client = createClient({
14472
+ const client = createClient({
14389
14473
  url: config.url,
14390
14474
  ...config.authToken ? { authToken: config.authToken } : {},
14391
14475
  ...config.syncUrl ? { syncUrl: config.syncUrl } : {},
14392
14476
  ...config.syncInterval !== void 0 ? { syncInterval: config.syncInterval } : {},
14393
14477
  ...this.isLocalDb ? { timeout: this.connectionTimeoutMs } : {}
14394
14478
  });
14479
+ this.client = isSingleConnectionDatabase(config) ? gateSingleConnectionClient(client) : client;
14395
14480
  this.pragmasReady = this.isLocalDb ? this.applyLocalPragmas() : Promise.resolve();
14396
14481
  } else {
14397
14482
  this.client = config.client;