@tursodatabase/serverless 0.2.1 → 0.2.3

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,6 @@
1
+ export declare class AsyncLock {
2
+ private locked;
3
+ private queue;
4
+ acquire(): Promise<void>;
5
+ release(): void;
6
+ }
@@ -0,0 +1,22 @@
1
+ export class AsyncLock {
2
+ constructor() {
3
+ this.locked = false;
4
+ this.queue = [];
5
+ }
6
+ async acquire() {
7
+ if (!this.locked) {
8
+ this.locked = true;
9
+ return;
10
+ }
11
+ return new Promise(resolve => { this.queue.push(resolve); });
12
+ }
13
+ release() {
14
+ const next = this.queue.shift();
15
+ if (next) {
16
+ next();
17
+ }
18
+ else {
19
+ this.locked = false;
20
+ }
21
+ }
22
+ }
package/dist/compat.d.ts CHANGED
@@ -76,11 +76,15 @@ export interface ResultSet {
76
76
  * libSQL-compatible error class with error codes.
77
77
  */
78
78
  export declare class LibsqlError extends Error {
79
- /** Machine-readable error code */
79
+ /** Machine-readable error code (e.g., "SQLITE_CONSTRAINT") */
80
80
  code: string;
81
+ /** Extended error code with more specific information (e.g., "SQLITE_CONSTRAINT_PRIMARYKEY") */
82
+ extendedCode?: string;
81
83
  /** Raw numeric error code (if available) */
82
84
  rawCode?: number;
83
- constructor(message: string, code: string, rawCode?: number);
85
+ /** Original error that caused this error */
86
+ cause?: Error;
87
+ constructor(message: string, code: string, extendedCode?: string, rawCode?: number, cause?: Error);
84
88
  }
85
89
  /**
86
90
  * Interactive transaction interface (not implemented in serverless mode).
package/dist/compat.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { Session } from './session.js';
2
+ import { DatabaseError } from './error.js';
2
3
  /**
3
4
  * libSQL-compatible error class with error codes.
4
5
  */
5
6
  export class LibsqlError extends Error {
6
- constructor(message, code, rawCode) {
7
+ constructor(message, code, extendedCode, rawCode, cause) {
7
8
  super(message);
8
9
  this.name = 'LibsqlError';
9
10
  this.code = code;
11
+ this.extendedCode = extendedCode;
10
12
  this.rawCode = rawCode;
13
+ this.cause = cause;
11
14
  }
12
15
  }
13
16
  class LibSQLClient {
@@ -114,7 +117,10 @@ class LibSQLClient {
114
117
  return this.convertResult(result);
115
118
  }
116
119
  catch (error) {
117
- throw new LibsqlError(error.message, "EXECUTE_ERROR");
120
+ if (error instanceof LibsqlError) {
121
+ throw error;
122
+ }
123
+ throw mapDatabaseError(error, "EXECUTE_ERROR");
118
124
  }
119
125
  }
120
126
  async batch(stmts, mode) {
@@ -131,7 +137,10 @@ class LibSQLClient {
131
137
  return [this.convertResult(result)];
132
138
  }
133
139
  catch (error) {
134
- throw new LibsqlError(error.message, "BATCH_ERROR");
140
+ if (error instanceof LibsqlError) {
141
+ throw error;
142
+ }
143
+ throw mapDatabaseError(error, "BATCH_ERROR");
135
144
  }
136
145
  }
137
146
  async migrate(stmts) {
@@ -149,7 +158,10 @@ class LibSQLClient {
149
158
  await this.session.sequence(sql);
150
159
  }
151
160
  catch (error) {
152
- throw new LibsqlError(error.message, "EXECUTE_MULTIPLE_ERROR");
161
+ if (error instanceof LibsqlError) {
162
+ throw error;
163
+ }
164
+ throw mapDatabaseError(error, "EXECUTE_MULTIPLE_ERROR");
153
165
  }
154
166
  }
155
167
  async sync() {
@@ -190,3 +202,63 @@ class LibSQLClient {
190
202
  export function createClient(config) {
191
203
  return new LibSQLClient(config);
192
204
  }
205
+ // Known SQLite base error code names, used to split extended codes like
206
+ // "SQLITE_CONSTRAINT_PRIMARYKEY" into base ("SQLITE_CONSTRAINT") and extended.
207
+ const sqliteBaseErrorCodes = new Set([
208
+ "SQLITE_ERROR",
209
+ "SQLITE_INTERNAL",
210
+ "SQLITE_PERM",
211
+ "SQLITE_ABORT",
212
+ "SQLITE_BUSY",
213
+ "SQLITE_LOCKED",
214
+ "SQLITE_NOMEM",
215
+ "SQLITE_READONLY",
216
+ "SQLITE_INTERRUPT",
217
+ "SQLITE_IOERR",
218
+ "SQLITE_CORRUPT",
219
+ "SQLITE_NOTFOUND",
220
+ "SQLITE_FULL",
221
+ "SQLITE_CANTOPEN",
222
+ "SQLITE_PROTOCOL",
223
+ "SQLITE_EMPTY",
224
+ "SQLITE_SCHEMA",
225
+ "SQLITE_TOOBIG",
226
+ "SQLITE_CONSTRAINT",
227
+ "SQLITE_MISMATCH",
228
+ "SQLITE_MISUSE",
229
+ "SQLITE_NOLFS",
230
+ "SQLITE_AUTH",
231
+ "SQLITE_FORMAT",
232
+ "SQLITE_RANGE",
233
+ "SQLITE_NOTADB",
234
+ "SQLITE_NOTICE",
235
+ "SQLITE_WARNING",
236
+ ]);
237
+ /**
238
+ * Parse a protocol error code into base and extended codes.
239
+ *
240
+ * The server may send either a base code ("SQLITE_CONSTRAINT") or an extended
241
+ * code ("SQLITE_CONSTRAINT_PRIMARYKEY"). This function splits them so that
242
+ * `code` is always the base code and `extendedCode` carries the full detail.
243
+ */
244
+ function parseErrorCode(serverCode) {
245
+ if (sqliteBaseErrorCodes.has(serverCode)) {
246
+ return { code: serverCode };
247
+ }
248
+ // Try to find a base code prefix (e.g. "SQLITE_CONSTRAINT" in "SQLITE_CONSTRAINT_PRIMARYKEY")
249
+ for (const base of sqliteBaseErrorCodes) {
250
+ if (serverCode.startsWith(base + "_")) {
251
+ return { code: base, extendedCode: serverCode };
252
+ }
253
+ }
254
+ // Unknown code — return as-is
255
+ return { code: serverCode };
256
+ }
257
+ function mapDatabaseError(error, fallbackCode) {
258
+ if (error instanceof DatabaseError && error.code) {
259
+ const { code, extendedCode } = parseErrorCode(error.code);
260
+ return new LibsqlError(error.message, code, extendedCode, error.rawCode, error);
261
+ }
262
+ const cause = error instanceof Error ? error : undefined;
263
+ return new LibsqlError(error.message ?? String(error), fallbackCode, undefined, undefined, cause);
264
+ }
@@ -9,7 +9,45 @@ export interface Config extends SessionConfig {
9
9
  * A connection to a Turso database.
10
10
  *
11
11
  * Provides methods for executing SQL statements and managing prepared statements.
12
- * Uses the SQL over HTTP protocol with streaming cursor support for optimal performance.
12
+ * Uses the SQL over HTTP protocol with streaming cursor support.
13
+ *
14
+ * ## Concurrency model
15
+ *
16
+ * A Connection is **single-stream**: it can only run one statement at a time.
17
+ * This is not an implementation quirk — it follows from the SQL over HTTP protocol,
18
+ * where each request carries a baton from the previous response to sequence operations
19
+ * on the server. Concurrent calls on the same connection would race on that baton
20
+ * and corrupt the stream. This is the same model as SQLite itself (one execution
21
+ * at a time per connection).
22
+ *
23
+ * If you call `execute()` while another is in flight, the call automatically
24
+ * waits for the previous one to finish — just like the native
25
+ * `@tursodatabase/database` binding.
26
+ *
27
+ * ## Parallel queries
28
+ *
29
+ * For parallelism, create multiple connections. `connect()` is cheap — it just
30
+ * allocates a config object. No TCP connection is opened until the first `execute()`,
31
+ * and the underlying `fetch()` runtime automatically pools and reuses TCP/TLS
32
+ * connections to the same origin.
33
+ *
34
+ * ```typescript
35
+ * import { connect } from "@tursodatabase/serverless";
36
+ *
37
+ * const config = { url: process.env.TURSO_URL, authToken: process.env.TURSO_TOKEN };
38
+ *
39
+ * // Option 1: one connection per parallel query
40
+ * const [users, orders] = await Promise.all([
41
+ * connect(config).execute("SELECT * FROM users WHERE active = 1"),
42
+ * connect(config).execute("SELECT * FROM orders WHERE status = 'pending'"),
43
+ * ]);
44
+ *
45
+ * // Option 2: reusable pool for repeated parallel work
46
+ * const pool = Array.from({ length: 4 }, () => connect(config));
47
+ * const results = await Promise.all(
48
+ * queries.map((sql, i) => pool[i % pool.length].execute(sql))
49
+ * );
50
+ * ```
13
51
  */
14
52
  export declare class Connection {
15
53
  private config;
@@ -17,6 +55,7 @@ export declare class Connection {
17
55
  private isOpen;
18
56
  private defaultSafeIntegerMode;
19
57
  private _inTransaction;
58
+ private execLock;
20
59
  constructor(config: Config);
21
60
  /**
22
61
  * Whether the database is currently in a transaction.
@@ -126,17 +165,32 @@ export declare class Connection {
126
165
  /**
127
166
  * Create a new connection to a Turso database.
128
167
  *
129
- * @param config - Configuration object with database URL and auth token
130
- * @returns A new Connection instance
168
+ * This is a lightweight operation it only allocates a config object. No network
169
+ * I/O happens until the first query. The underlying `fetch()` implementation
170
+ * automatically pools TCP/TLS connections to the same origin, so creating many
171
+ * connections is cheap.
172
+ *
173
+ * Each connection is single-stream: concurrent calls on the same connection are
174
+ * automatically serialized. For true parallelism, create multiple connections:
131
175
  *
132
- * @example
133
176
  * ```typescript
134
177
  * import { connect } from "@tursodatabase/serverless";
135
178
  *
136
- * const client = connect({
137
- * url: process.env.TURSO_DATABASE_URL,
138
- * authToken: process.env.TURSO_AUTH_TOKEN
139
- * });
179
+ * const config = { url: process.env.TURSO_URL, authToken: process.env.TURSO_TOKEN };
180
+ *
181
+ * // Sequential (single connection is fine)
182
+ * const conn = connect(config);
183
+ * const a = await conn.execute("SELECT 1");
184
+ * const b = await conn.execute("SELECT 2");
185
+ *
186
+ * // Parallel (use separate connections)
187
+ * const [x, y] = await Promise.all([
188
+ * connect(config).execute("SELECT 1"),
189
+ * connect(config).execute("SELECT 2"),
190
+ * ]);
140
191
  * ```
192
+ *
193
+ * @param config - Configuration object with database URL and auth token
194
+ * @returns A new Connection instance
141
195
  */
142
196
  export declare function connect(config: Config): Connection;
@@ -1,16 +1,56 @@
1
+ import { AsyncLock } from './async-lock.js';
1
2
  import { Session } from './session.js';
2
3
  import { Statement } from './statement.js';
3
4
  /**
4
5
  * A connection to a Turso database.
5
6
  *
6
7
  * Provides methods for executing SQL statements and managing prepared statements.
7
- * Uses the SQL over HTTP protocol with streaming cursor support for optimal performance.
8
+ * Uses the SQL over HTTP protocol with streaming cursor support.
9
+ *
10
+ * ## Concurrency model
11
+ *
12
+ * A Connection is **single-stream**: it can only run one statement at a time.
13
+ * This is not an implementation quirk — it follows from the SQL over HTTP protocol,
14
+ * where each request carries a baton from the previous response to sequence operations
15
+ * on the server. Concurrent calls on the same connection would race on that baton
16
+ * and corrupt the stream. This is the same model as SQLite itself (one execution
17
+ * at a time per connection).
18
+ *
19
+ * If you call `execute()` while another is in flight, the call automatically
20
+ * waits for the previous one to finish — just like the native
21
+ * `@tursodatabase/database` binding.
22
+ *
23
+ * ## Parallel queries
24
+ *
25
+ * For parallelism, create multiple connections. `connect()` is cheap — it just
26
+ * allocates a config object. No TCP connection is opened until the first `execute()`,
27
+ * and the underlying `fetch()` runtime automatically pools and reuses TCP/TLS
28
+ * connections to the same origin.
29
+ *
30
+ * ```typescript
31
+ * import { connect } from "@tursodatabase/serverless";
32
+ *
33
+ * const config = { url: process.env.TURSO_URL, authToken: process.env.TURSO_TOKEN };
34
+ *
35
+ * // Option 1: one connection per parallel query
36
+ * const [users, orders] = await Promise.all([
37
+ * connect(config).execute("SELECT * FROM users WHERE active = 1"),
38
+ * connect(config).execute("SELECT * FROM orders WHERE status = 'pending'"),
39
+ * ]);
40
+ *
41
+ * // Option 2: reusable pool for repeated parallel work
42
+ * const pool = Array.from({ length: 4 }, () => connect(config));
43
+ * const results = await Promise.all(
44
+ * queries.map((sql, i) => pool[i % pool.length].execute(sql))
45
+ * );
46
+ * ```
8
47
  */
9
48
  export class Connection {
10
49
  constructor(config) {
11
50
  this.isOpen = true;
12
51
  this.defaultSafeIntegerMode = false;
13
52
  this._inTransaction = false;
53
+ this.execLock = new AsyncLock();
14
54
  if (!config.url) {
15
55
  throw new Error("invalid config: url is required");
16
56
  }
@@ -75,7 +115,13 @@ export class Connection {
75
115
  if (!this.isOpen) {
76
116
  throw new TypeError("The database connection is not open");
77
117
  }
78
- return this.session.execute(sql, args || [], this.defaultSafeIntegerMode);
118
+ await this.execLock.acquire();
119
+ try {
120
+ return await this.session.execute(sql, args || [], this.defaultSafeIntegerMode);
121
+ }
122
+ finally {
123
+ this.execLock.release();
124
+ }
79
125
  }
80
126
  /**
81
127
  * Execute a SQL statement and return all results.
@@ -93,7 +139,13 @@ export class Connection {
93
139
  if (!this.isOpen) {
94
140
  throw new TypeError("The database connection is not open");
95
141
  }
96
- return this.session.sequence(sql);
142
+ await this.execLock.acquire();
143
+ try {
144
+ return await this.session.sequence(sql);
145
+ }
146
+ finally {
147
+ this.execLock.release();
148
+ }
97
149
  }
98
150
  /**
99
151
  * Execute multiple SQL statements in a batch.
@@ -112,7 +164,16 @@ export class Connection {
112
164
  * ```
113
165
  */
114
166
  async batch(statements, mode) {
115
- return this.session.batch(statements);
167
+ if (!this.isOpen) {
168
+ throw new TypeError("The database connection is not open");
169
+ }
170
+ await this.execLock.acquire();
171
+ try {
172
+ return await this.session.batch(statements);
173
+ }
174
+ finally {
175
+ this.execLock.release();
176
+ }
116
177
  }
117
178
  /**
118
179
  * Execute a pragma.
@@ -124,8 +185,14 @@ export class Connection {
124
185
  if (!this.isOpen) {
125
186
  throw new TypeError("The database connection is not open");
126
187
  }
127
- const sql = `PRAGMA ${pragma}`;
128
- return this.session.execute(sql);
188
+ await this.execLock.acquire();
189
+ try {
190
+ const sql = `PRAGMA ${pragma}`;
191
+ return await this.session.execute(sql);
192
+ }
193
+ finally {
194
+ this.execLock.release();
195
+ }
129
196
  }
130
197
  /**
131
198
  * Sets the default safe integers mode for all statements from this connection.
@@ -212,18 +279,33 @@ export class Connection {
212
279
  /**
213
280
  * Create a new connection to a Turso database.
214
281
  *
215
- * @param config - Configuration object with database URL and auth token
216
- * @returns A new Connection instance
282
+ * This is a lightweight operation it only allocates a config object. No network
283
+ * I/O happens until the first query. The underlying `fetch()` implementation
284
+ * automatically pools TCP/TLS connections to the same origin, so creating many
285
+ * connections is cheap.
286
+ *
287
+ * Each connection is single-stream: concurrent calls on the same connection are
288
+ * automatically serialized. For true parallelism, create multiple connections:
217
289
  *
218
- * @example
219
290
  * ```typescript
220
291
  * import { connect } from "@tursodatabase/serverless";
221
292
  *
222
- * const client = connect({
223
- * url: process.env.TURSO_DATABASE_URL,
224
- * authToken: process.env.TURSO_AUTH_TOKEN
225
- * });
293
+ * const config = { url: process.env.TURSO_URL, authToken: process.env.TURSO_TOKEN };
294
+ *
295
+ * // Sequential (single connection is fine)
296
+ * const conn = connect(config);
297
+ * const a = await conn.execute("SELECT 1");
298
+ * const b = await conn.execute("SELECT 2");
299
+ *
300
+ * // Parallel (use separate connections)
301
+ * const [x, y] = await Promise.all([
302
+ * connect(config).execute("SELECT 1"),
303
+ * connect(config).execute("SELECT 2"),
304
+ * ]);
226
305
  * ```
306
+ *
307
+ * @param config - Configuration object with database URL and auth token
308
+ * @returns A new Connection instance
227
309
  */
228
310
  export function connect(config) {
229
311
  return new Connection(config);
package/dist/error.d.ts CHANGED
@@ -1,3 +1,9 @@
1
1
  export declare class DatabaseError extends Error {
2
- constructor(message: string);
2
+ /** Machine-readable error code (e.g., "SQLITE_CONSTRAINT") */
3
+ code?: string;
4
+ /** Raw numeric error code */
5
+ rawCode?: number;
6
+ /** Original error that caused this error */
7
+ cause?: Error;
8
+ constructor(message: string, code?: string, rawCode?: number, cause?: Error);
3
9
  }
package/dist/error.js CHANGED
@@ -1,7 +1,10 @@
1
1
  export class DatabaseError extends Error {
2
- constructor(message) {
2
+ constructor(message, code, rawCode, cause) {
3
3
  super(message);
4
4
  this.name = 'DatabaseError';
5
+ this.code = code;
6
+ this.rawCode = rawCode;
7
+ this.cause = cause;
5
8
  Object.setPrototypeOf(this, DatabaseError.prototype);
6
9
  }
7
10
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Connection, connect, type Config } from './connection.js';
2
2
  export { Statement } from './statement.js';
3
+ export { Session, type SessionConfig } from './session.js';
3
4
  export { DatabaseError } from './error.js';
4
5
  export { type Column, ENCRYPTION_KEY_HEADER } from './protocol.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Turso serverless driver entry point
2
2
  export { Connection, connect } from './connection.js';
3
3
  export { Statement } from './statement.js';
4
+ export { Session } from './session.js';
4
5
  export { DatabaseError } from './error.js';
5
6
  export { ENCRYPTION_KEY_HEADER } from './protocol.js';
package/dist/session.js CHANGED
@@ -41,7 +41,7 @@ export class Session {
41
41
  if (response.results && response.results[0]) {
42
42
  const result = response.results[0];
43
43
  if (result.type === "error") {
44
- throw new DatabaseError(result.error?.message || 'Describe execution failed');
44
+ throw new DatabaseError(result.error?.message || 'Describe execution failed', result.error?.code);
45
45
  }
46
46
  if (result.response?.type === "describe" && result.response.result) {
47
47
  return result.response.result;
@@ -162,7 +162,7 @@ export class Session {
162
162
  break;
163
163
  case 'step_error':
164
164
  case 'error':
165
- throw new DatabaseError(entry.error?.message || 'SQL execution failed');
165
+ throw new DatabaseError(entry.error?.message || 'SQL execution failed', entry.error?.code);
166
166
  }
167
167
  }
168
168
  return {
@@ -235,7 +235,7 @@ export class Session {
235
235
  break;
236
236
  case 'step_error':
237
237
  case 'error':
238
- throw new DatabaseError(entry.error?.message || 'Batch execution failed');
238
+ throw new DatabaseError(entry.error?.message || 'Batch execution failed', entry.error?.code);
239
239
  }
240
240
  }
241
241
  return {
@@ -266,7 +266,7 @@ export class Session {
266
266
  if (response.results && response.results[0]) {
267
267
  const result = response.results[0];
268
268
  if (result.type === "error") {
269
- throw new DatabaseError(result.error?.message || 'Sequence execution failed');
269
+ throw new DatabaseError(result.error?.message || 'Sequence execution failed', result.error?.code);
270
270
  }
271
271
  }
272
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tursodatabase/serverless",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",