// @effect-diagnostics nodeBuiltinImport:offimport * as NodeChildProcess from "node:child_process";import * as NodeFS from "node:fs";import * as NodeOS from "node:os";import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as SqlClient from "effect/unstable/sql/SqlClient"; import { SqlitePersistenceMemory, makeSqlitePersistenceLive } from "./Sqlite.ts"; const lockHolderSource = `const { DatabaseSync } = require("node:sqlite");const db = new DatabaseSync(process.argv[1]);db.exec("BEGIN IMMEDIATE");process.stdout.write("locked\\n");setTimeout(() => { db.exec("COMMIT"); db.close();}, Number(process.argv[2]));`; const spawnWriteLockHolder = (dbPath: string, holdMs: number) => Effect.promise( () => new Promise<void>((resolve, reject) => { const holder = NodeChildProcess.spawn( process.execPath, ["-e", lockHolderSource, dbPath, String(holdMs)], { stdio: ["ignore", "pipe", "ignore"] }, ); holder.stdout.once("data", () => resolve()); holder.on("error", reject); holder.on("exit", () => reject(new Error("lock holder exited before acquiring the write lock")), ); }), ); it.effect("waits out a concurrent writer instead of failing with SQLITE_BUSY", () => { const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-busy-")); const dbPath = NodePath.join(tempDir, "state.sqlite"); return Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* sql`CREATE TABLE busy_probe(id INTEGER PRIMARY KEY)`; yield* spawnWriteLockHolder(dbPath, 300); yield* sql`INSERT INTO busy_probe(id) VALUES (${1})`; const rows = yield* sql<{ readonly id: number }>`SELECT id FROM busy_probe`; assert.deepEqual([...rows], [{ id: 1 }]); }).pipe( Effect.provide(makeSqlitePersistenceLive(dbPath).pipe(Layer.provide(NodeServices.layer))), Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))), );}); it.effect("applies busy_timeout in the shared persistence setup", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const rows = yield* sql<{ readonly timeout: number }>`PRAGMA busy_timeout`; assert.equal(rows[0]?.timeout, 5000); }).pipe(Effect.provide(SqlitePersistenceMemory)),);
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as SqlClient from "effect/unstable/sql/SqlClient";
import { SqlitePersistenceMemory, makeSqlitePersistenceLive } from "./Sqlite.ts";
const lockHolderSource = `
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(process.argv[1]);
db.exec("BEGIN IMMEDIATE");
process.stdout.write("locked\\n");
setTimeout(() => {
db.exec("COMMIT");
db.close();
}, Number(process.argv[2]));
`;
const spawnWriteLockHolder = (dbPath: string, holdMs: number) =>
Effect.promise(
() =>
new Promise<void>((resolve, reject) => {
const holder = NodeChildProcess.spawn(
process.execPath,
["-e", lockHolderSource, dbPath, String(holdMs)],
{ stdio: ["ignore", "pipe", "ignore"] },
);
holder.stdout.once("data", () => resolve());
holder.on("error", reject);
holder.on("exit", () =>
reject(new Error("lock holder exited before acquiring the write lock")),
);
}),
);
it.effect("waits out a concurrent writer instead of failing with SQLITE_BUSY", () => {
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-busy-"));
const dbPath = NodePath.join(tempDir, "state.sqlite");
return Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* sql`CREATE TABLE busy_probe(id INTEGER PRIMARY KEY)`;
yield* spawnWriteLockHolder(dbPath, 300);
yield* sql`INSERT INTO busy_probe(id) VALUES (${1})`;
const rows = yield* sql<{ readonly id: number }>`SELECT id FROM busy_probe`;
assert.deepEqual([...rows], [{ id: 1 }]);
}).pipe(
Effect.provide(makeSqlitePersistenceLive(dbPath).pipe(Layer.provide(NodeServices.layer))),
Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
);
});
it.effect("applies busy_timeout in the shared persistence setup", () =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
const rows = yield* sql<{ readonly timeout: number }>`PRAGMA busy_timeout`;
assert.equal(rows[0]?.timeout, 5000);
}).pipe(Effect.provide(SqlitePersistenceMemory)),
);