refactor(durable): drop AsyncLocalStorage misuse detection from Node SQLite adapter

This commit is contained in:
Christian Klotz
2026-09-30 20:38:41 +01:00
parent 77ac62f22a
commit a0feabda7c
3 changed files with 3 additions and 35 deletions
@@ -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.
*
+1 -19
View File
@@ -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<TransactionScope>();
private closed = false;
constructor(database: DatabaseSync) {
@@ -106,16 +103,11 @@ export class NodeSqliteDatabase extends NodeSqliteExecutor implements SqliteData
}
transaction<T>(callback: (transaction: SqliteExecutor) => Promise<T>): Promise<T> {
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<void> {
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<T>(operation: () => T): Promise<T> {
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. */
@@ -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)");