From a0feabda7ce19ba7155c57775519640606e74a1e Mon Sep 17 00:00:00 2001 From: Christian Klotz Date: Wed, 30 Sep 2026 20:38:41 +0100 Subject: [PATCH] refactor(durable): drop AsyncLocalStorage misuse detection from Node SQLite adapter --- .../durable/src/storage/sqlite/database.ts | 3 ++- packages/durable/src/storage/sqlite/node.ts | 20 +------------------ packages/durable/test/sqlite-facade.test.ts | 15 -------------- 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/durable/src/storage/sqlite/database.ts b/packages/durable/src/storage/sqlite/database.ts index 35a0f39b6..0ab6d0024 100644 --- a/packages/durable/src/storage/sqlite/database.ts +++ b/packages/durable/src/storage/sqlite/database.ts @@ -21,7 +21,8 @@ export interface SqliteExecutor { * All operations are asynchronous so adapters may execute outside the harness runtime. * * `transaction` passes the callback a transaction handle. All work in the transaction - * must use that handle; the handle is invalid after the callback settles. Adapters must + * must use that handle; calling the database itself from inside the callback waits for the + * transaction and never completes. The handle is invalid after the callback settles. Adapters must * queue unrelated operations and other transactions until the transaction finishes. The * returned promise settles after commit or rollback. * diff --git a/packages/durable/src/storage/sqlite/node.ts b/packages/durable/src/storage/sqlite/node.ts index 74c3cdb1e..25e6de655 100644 --- a/packages/durable/src/storage/sqlite/node.ts +++ b/packages/durable/src/storage/sqlite/node.ts @@ -1,4 +1,3 @@ -import { AsyncLocalStorage } from "node:async_hooks"; import { mkdir } from "node:fs/promises"; import { dirname } from "node:path"; import type { StatementSync } from "node:sqlite"; @@ -97,8 +96,6 @@ class NodeSqliteTransaction extends NodeSqliteExecutor { /** `SqliteDatabase` adapter backed by Node's built-in `node:sqlite`. */ export class NodeSqliteDatabase extends NodeSqliteExecutor implements SqliteDatabase { private readonly access = new SerialOperationQueue(); - /** Detects database calls from inside a transaction callback, which would otherwise wait for that transaction forever. */ - private readonly transactionScope = new AsyncLocalStorage(); private closed = false; constructor(database: DatabaseSync) { @@ -106,16 +103,11 @@ export class NodeSqliteDatabase extends NodeSqliteExecutor implements SqliteData } transaction(callback: (transaction: SqliteExecutor) => Promise): Promise { - if (this.insideTransaction()) { - return Promise.reject(new Error("Nested SQLite transactions are not supported")); - } return this.access.run(async () => { this.database.exec("BEGIN IMMEDIATE"); const scope = { active: true }; try { - const result = await this.transactionScope.run(scope, () => - callback(new NodeSqliteTransaction(this.database, this.statements, scope)), - ); + const result = await callback(new NodeSqliteTransaction(this.database, this.statements, scope)); scope.active = false; this.database.exec("COMMIT"); return result; @@ -132,9 +124,6 @@ export class NodeSqliteDatabase extends NodeSqliteExecutor implements SqliteData } close(): Promise { - if (this.insideTransaction()) { - return Promise.reject(new Error("Cannot close SQLite during an active transaction")); - } return this.access.run(() => { if (this.closed) return; this.closed = true; @@ -148,15 +137,8 @@ export class NodeSqliteDatabase extends NodeSqliteExecutor implements SqliteData } protected runOperation(operation: () => T): Promise { - if (this.insideTransaction()) { - return Promise.reject(new Error("Use the transaction handle inside a transaction callback")); - } return this.access.run(operation); } - - private insideTransaction(): boolean { - return this.transactionScope.getStore()?.active === true; - } } /** Open and configure a Node-backed SQLite database facade. */ diff --git a/packages/durable/test/sqlite-facade.test.ts b/packages/durable/test/sqlite-facade.test.ts index b9a311b26..566c2a687 100644 --- a/packages/durable/test/sqlite-facade.test.ts +++ b/packages/durable/test/sqlite-facade.test.ts @@ -193,21 +193,6 @@ describe("portable SQLite facade settlement", () => { await database.close(); }); - it("rejects database operations from inside a transaction callback instead of waiting forever", async () => { - const database = await openNodeSqliteDatabase(":memory:"); - await database.exec("CREATE TABLE misuse_probe (value INTEGER)"); - const misuse = "Use the transaction handle inside a transaction callback"; - await expect(database.transaction(() => database.exec("SELECT 1"))).rejects.toThrow(misuse); - await expect(database.transaction(() => database.all("SELECT value FROM misuse_probe"))).rejects.toThrow(misuse); - await expect(database.transaction(() => database.transaction(async () => undefined))).rejects.toThrow( - "Nested SQLite transactions are not supported", - ); - await expect(database.transaction(() => database.close())).rejects.toThrow( - "Cannot close SQLite during an active transaction", - ); - await database.close(); - }); - it("rejects a transaction handle used after its transaction settles", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.exec("CREATE TABLE stale_probe (value INTEGER)");