Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/libsql-client/src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,79 @@ describe("transaction()", () => {
}
});

describe("timeout option (local files)", () => {
const dbPath = `/tmp/libsql-client-timeout-test-${process.pid}.db`;
const dbUrl = `file:${dbPath}`;

afterEach(async () => {
const fs = await import("node:fs");
for (const suffix of ["", "-wal", "-shm"]) {
fs.rmSync(dbPath + suffix, { force: true });
}
});

test("timeout sets busy_timeout on the connection", async () => {
const c = createClient({ url: dbUrl, timeout: 5000 });
try {
const rs = await c.execute("PRAGMA busy_timeout");
expect(rs.rows[0]["timeout"]).toStrictEqual(5000);
} finally {
c.close();
}
});

// see issue https://github.com/tursodatabase/libsql-client-ts/issues/288
test("timeout survives connections created after transaction()", async () => {
const c = createClient({ url: dbUrl, timeout: 5000 });
try {
const txn = await c.transaction("write");
await txn.execute("CREATE TABLE t (a)");
await txn.commit();

// transaction() hands the client's connection to the transaction
// and lazily opens a new one; the timeout must apply to it too.
const rs = await c.execute("PRAGMA busy_timeout");
expect(rs.rows[0]["timeout"]).toStrictEqual(5000);
} finally {
c.close();
}
});

test("locked database waits for timeout instead of failing immediately", async () => {
const setup = createClient({ url: dbUrl });
await setup.execute("CREATE TABLE t (a)");
setup.close();

const lockHolder = createClient({ url: dbUrl });
const withTimeout = createClient({ url: dbUrl, timeout: 500 });
const withoutTimeout = createClient({ url: dbUrl });
try {
const lock = await lockHolder.transaction("write");
await lock.execute("INSERT INTO t VALUES (1)");

// Without a timeout, a locked write fails immediately.
let start = Date.now();
await expect(
withoutTimeout.execute("INSERT INTO t VALUES (2)"),
).rejects.toBeLibsqlError("SQLITE_BUSY");
expect(Date.now() - start).toBeLessThan(250);

// With a timeout, the busy handler waits before giving up.
start = Date.now();
await expect(
withTimeout.execute("INSERT INTO t VALUES (3)"),
).rejects.toBeLibsqlError("SQLITE_BUSY");
expect(Date.now() - start).toBeGreaterThanOrEqual(450);

await lock.rollback();
} finally {
lockHolder.close();
withTimeout.close();
withoutTimeout.close();
}
});
});

(isFile ? test : test.skip)("raw error codes", async () => {
const c = createClient(config);
try {
Expand Down
12 changes: 12 additions & 0 deletions packages/libsql-client/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ describe("expandConfig - parsing of valid arguments", () => {
concurrency: 20,
},
},
{
name: "timeout",
config: { url: "file:local.db", timeout: 5000 },
expanded: {
scheme: "file",
tls: true,
intMode: "number",
path: "local.db",
concurrency: 20,
timeout: 5000,
},
},
{
name: "override auth token",
config: {
Expand Down
1 change: 1 addition & 0 deletions packages/libsql-client/src/sqlite3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function _createClient(config: ExpandedConfig): Client {
syncPeriod: config.syncInterval,
readYourWrites: config.readYourWrites,
offline: config.offline,
timeout: config.timeout,
};

const db = new Database(path, options);
Expand Down
11 changes: 11 additions & 0 deletions packages/libsql-core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ export interface Config {
* number to increase the concurrency limit or set it to 0 to disable concurrency limits completely.
*/
concurrency?: number | undefined;

/** Busy timeout in milliseconds for local `file:` databases.
*
* When another connection holds a lock on the database file, operations wait up to this many
* milliseconds before failing with `SQLITE_BUSY`. By default, operations fail immediately.
*
* This option is applied to every connection the client opens, including connections created
* internally after `transaction()`. It only takes effect for local SQLite databases; remote
* clients ignore it.
*/
timeout?: number;
}

/** Representation of integers from database as JavaScript values. See {@link Config.intMode}. */
Expand Down
3 changes: 3 additions & 0 deletions packages/libsql-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface ExpandedConfig {
intMode: IntMode;
fetch: Function | undefined;
concurrency: number;
timeout: number | undefined;
}

export type ExpandedScheme = "wss" | "ws" | "https" | "http" | "file";
Expand Down Expand Up @@ -180,6 +181,7 @@ export function expandConfig(
readYourWrites: config.readYourWrites,
offline: config.offline,
fetch: config.fetch,
timeout: config.timeout,
authToken: undefined,
encryptionKey: undefined,
remoteEncryptionKey: undefined,
Expand All @@ -202,5 +204,6 @@ export function expandConfig(
readYourWrites: config.readYourWrites,
offline: config.offline,
fetch: config.fetch,
timeout: config.timeout,
};
}
Loading