diff --git a/.changeset/fix-d1-concurrent-queries.md b/.changeset/fix-d1-concurrent-queries.md new file mode 100644 index 0000000000..2beac9631c --- /dev/null +++ b/.changeset/fix-d1-concurrent-queries.md @@ -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. diff --git a/e2e/smoke/test/fixtures/cloudflare/package.json b/e2e/smoke/test/fixtures/cloudflare/package.json index ecde131590..9aac3ba7f0 100644 --- a/e2e/smoke/test/fixtures/cloudflare/package.json +++ b/e2e/smoke/test/fixtures/cloudflare/package.json @@ -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" }, diff --git a/e2e/smoke/test/fixtures/cloudflare/src/index.ts b/e2e/smoke/test/fixtures/cloudflare/src/index.ts index ee93bf90cd..ad5f63d0b6 100644 --- a/e2e/smoke/test/fixtures/cloudflare/src/index.ts +++ b/e2e/smoke/test/fixtures/cloudflare/src/index.ts @@ -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(); diff --git a/e2e/smoke/test/fixtures/cloudflare/test/d1-concurrency.test.ts b/e2e/smoke/test/fixtures/cloudflare/test/d1-concurrency.test.ts new file mode 100644 index 0000000000..7106e642ab --- /dev/null +++ b/e2e/smoke/test/fixtures/cloudflare/test/d1-concurrency.test.ts @@ -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); diff --git a/e2e/smoke/test/fixtures/cloudflare/wrangler.json b/e2e/smoke/test/fixtures/cloudflare/wrangler.json index 0480ffa700..97d701e522 100644 --- a/e2e/smoke/test/fixtures/cloudflare/wrangler.json +++ b/e2e/smoke/test/fixtures/cloudflare/wrangler.json @@ -5,6 +5,9 @@ "name": "cloudflare", "upload_source_maps": true, "compatibility_flags": ["nodejs_compat"], + "alias": { + "kysely": "kysely-029" + }, "d1_databases": [ { "binding": "DB", diff --git a/packages/kysely-adapter/src/d1-sqlite-dialect.ts b/packages/kysely-adapter/src/d1-sqlite-dialect.ts index 3dc4250a4d..6006b1067b 100644 --- a/packages/kysely-adapter/src/d1-sqlite-dialect.ts +++ b/packages/kysely-adapter/src/d1-sqlite-dialect.ts @@ -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. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6df4fb7b8..d9448cce67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: