fix(kysely-adapter): allow concurrent queries on the D1 dialect (#11490)

Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
Aryan
2026-10-10 07:59:43 +00:00
committed by GitHub
co-authored by Taesu
parent e15625ff38
commit 9904b29ece
7 changed files with 134 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@better-auth/kysely-adapter": patch
---
Allow concurrent D1 queries on a shared auth instance with Kysely 0.29, avoiding cross-request I/O errors.
+2
View File
@@ -3,11 +3,13 @@
"private": true,
"dependencies": {
"@better-auth/core": "workspace:*",
"@better-auth/kysely-adapter": "workspace:*",
"@better-auth/sso": "workspace:*",
"better-auth": "workspace:*",
"hono": "^4.13.7"
},
"devDependencies": {
"kysely-029": "npm:kysely@0.29.6",
"vitest": "catalog:vitest",
"wrangler": "4.123.0"
},
+57
View File
@@ -1,3 +1,4 @@
import { env } from "cloudflare:workers";
import type { AuthEndpointContext } from "@better-auth/core/context";
import {
getCurrentAdapter,
@@ -9,12 +10,48 @@ import type {
DBAdapter,
DBTransactionAdapter,
} from "@better-auth/core/db/adapter";
import { createKyselyAdapter } from "@better-auth/kysely-adapter";
import { getMigrations } from "better-auth/db/migration";
import { Hono } from "hono";
import { auth } from "./auth";
const app = new Hono();
let slowQueryStarted = false;
let releaseSlowQuery = false;
const delayedDatabase = {
prepare(query: string) {
const statement = env.DB.prepare(query);
return {
bind(...parameters: unknown[]) {
const bound = statement.bind(...parameters);
return {
async all() {
if (query.includes("slow_marker")) {
slowQueryStarted = true;
try {
while (!releaseSlowQuery) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
} finally {
slowQueryStarted = false;
releaseSlowQuery = false;
}
}
return bound.all();
},
};
},
};
},
batch: (...statements: D1PreparedStatement[]) => env.DB.batch(statements),
exec: (query: string) => env.DB.exec(query),
} as unknown as D1Database;
const { kysely: concurrencyDatabase } = await createKyselyAdapter({
database: delayedDatabase,
});
if (!concurrencyDatabase) throw new Error("D1 test database was not created");
app.all("/api/auth/*", (c) => auth.handler(c.req.raw));
app.get("/_test/session", async (c) => {
@@ -24,6 +61,26 @@ app.get("/_test/session", async (c) => {
return c.json(session);
});
app.get("/_test/d1-concurrency/:query", async (c) => {
if (c.req.param("query") === "status") {
return c.json({ slowQueryStarted });
}
if (c.req.param("query") === "release") {
releaseSlowQuery = true;
return c.body(null, 204);
}
const slow = c.req.param("query") === "slow";
const query = concurrencyDatabase.selectFrom("sqlite_master");
await (slow
? query.select("name as slow_marker")
: query.select("name as fast_marker")
)
.limit(1)
.execute();
return c.body(null, 204);
});
app.post("/_test/migrate", async (c) => {
const { runMigrations } = await getMigrations(auth.options);
await runMigrations();
@@ -0,0 +1,50 @@
import { afterAll, afterEach, beforeAll, expect, it, vi } from "vitest";
import { createTestHarness } from "wrangler";
const server = createTestHarness({
workers: [{ configPath: "./wrangler.json" }],
});
beforeAll(async () => {
await server.listen();
});
afterEach(({ task }) => {
if (task.result?.state === "fail") server.debug();
});
afterAll(async () => {
await server.close();
});
/**
* @see https://github.com/better-auth/better-auth/issues/11483
*/
it("lets independent D1 requests complete while another query is pending", async () => {
const slowRequest = server.fetch(
"http://localhost:8787/_test/d1-concurrency/slow",
);
let slowStatus: number;
try {
await vi.waitFor(
async () => {
const response = await server.fetch(
"http://localhost:8787/_test/d1-concurrency/status",
);
expect(await response.json()).toEqual({ slowQueryStarted: true });
},
{ timeout: 5000 },
);
const fastResponse = await server.fetch(
"http://localhost:8787/_test/d1-concurrency/fast",
{ signal: AbortSignal.timeout(5000) },
);
expect(fastResponse.status).toBe(204);
} finally {
await server.fetch("http://localhost:8787/_test/d1-concurrency/release");
slowStatus = (await slowRequest).status;
}
expect(slowStatus).toBe(204);
}, 15_000);
+3
View File
@@ -5,6 +5,9 @@
"name": "cloudflare",
"upload_source_maps": true,
"compatibility_flags": ["nodejs_compat"],
"alias": {
"kysely": "kysely-029"
},
"d1_databases": [
{
"binding": "DB",
@@ -81,7 +81,11 @@ export function createD1IndexIntrospector(
};
}
class D1SqliteAdapter extends SqliteAdapter {}
class D1SqliteAdapter extends SqliteAdapter {
get supportsMultipleConnections(): boolean {
return true;
}
}
/**
* Config for the D1 SQLite dialect.
+12
View File
@@ -887,6 +887,9 @@ importers:
'@better-auth/core':
specifier: workspace:*
version: link:../../../../../packages/core
'@better-auth/kysely-adapter':
specifier: workspace:*
version: link:../../../../../packages/kysely-adapter
'@better-auth/sso':
specifier: workspace:*
version: link:../../../../../packages/sso
@@ -897,6 +900,9 @@ importers:
specifier: ^4.13.7
version: 4.13.7
devDependencies:
kysely-029:
specifier: npm:kysely@0.29.6
version: kysely@0.29.6
vitest:
specifier: catalog:vitest
version: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@25.9.4)(@vitest/coverage-istanbul@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.8.9)(msw@2.14.6(@types/node@25.9.4)(typescript@6.0.3))(vite@8.2.0(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))
@@ -11275,6 +11281,10 @@ packages:
resolution: {integrity: sha512-VHtBdW6XB/pgoTSqraM3UAa2rYoYdNXqnNPpX+8XXP+cwYbVEFuAp3HyPt1vpNfU9l7Y2kpUrA9QDPsy8uUqOQ==}
engines: {node: '>=22.0.0'}
kysely@0.29.6:
resolution: {integrity: sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==}
engines: {node: '>=22.0.0'}
lan-network@0.1.7:
resolution: {integrity: sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==}
hasBin: true
@@ -27524,6 +27534,8 @@ snapshots:
kysely@0.29.3: {}
kysely@0.29.6: {}
lan-network@0.1.7: {}
launch-editor@2.14.1: