1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
5import * as SqlClient from "effect/sql/SqlClient";
6import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
7
8import { runMigrations } from "./Migrations.ts";
9import { initializeV2Database } from "./initializeV2Database.ts";
10import * as ServerConfig from "../config.ts";
11
12// Size the -wal file is cut back to on the first commit after a WAL reset.
13export const WAL_SIZE_LIMIT_BYTES = 32 * 1024 * 1024;
14
15const layerSetup = Layer.effectDiscard(
16 Effect.gen(function* () {
17 const sql = yield* SqlClient.SqlClient;
18 // CLI and server write from separate processes; wait rather than fail with SQLITE_BUSY.
19 yield* sql`PRAGMA busy_timeout = 5000;`;
20 yield* sql`PRAGMA foreign_keys = ON;`;
21 yield* sql`PRAGMA journal_mode = WAL;`;
22 // FULL fsyncs the WAL on every commit, on the main thread. Every provider
23 // event is its own commit, so a slow or busy disk stalls the event loop
24 // and clients drop. In WAL mode NORMAL only fsyncs at checkpoints. It
25 // cannot corrupt the database and an app crash loses nothing; an OS crash
26 // or power loss can roll back commits since the last checkpoint.
27 yield* sql`PRAGMA synchronous = NORMAL;`;
28 // Checkpoints are now the only sync, and macOS fsync() does not flush the
29 // drive cache. F_FULLFSYNC makes them reach disk; other platforms ignore it.
30 // Per-commit fullfsync stays off.
31 yield* sql`PRAGMA checkpoint_fullfsync = ON;`;
32 // PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its
33 // largest size until the last connection closes.
34 yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`);
35 yield* runMigrations();
36 // Without sqlite_stat1 the planner guesses at index selectivity. 0x10012
37 // analyzes tables whose indexes lack stats or whose row count changed 10x,
38 // under a temporary analysis_limit. Otherwise it only checks table sizes.
39 yield* sql`PRAGMA optimize = 0x10012;`;
40 }),
41);
42
43export const layerFromPath = Effect.fn("makeSqlitePersistenceLive")(function* (dbPath: string) {
44 const fs = yield* FileSystem.FileSystem;
45 const path = yield* Path.Path;
46 yield* fs.makeDirectory(path.dirname(dbPath), { recursive: true });
47
48 return Layer.provideMerge(
49 layerSetup,
50 NodeSqliteClient.layer({
51 filename: dbPath,
52 readerWorker: true,
53 // Checkpoints sync the database, with F_FULLFSYNC on macOS. Run them off
54 // the event loop so a slow flush cannot stall clients.
55 checkpointWorker: true,
56 spanAttributes: {
57 "db.name": path.basename(dbPath),
58 "service.name": "t3code-server",
59 },
60 }),
61 );
62}, Layer.unwrap);
63
64export const layerMemory = Layer.provideMerge(
65 layerSetup,
66 NodeSqliteClient.layer({ filename: ":memory:" }),
67);
68
69export const layerConfig = Layer.unwrap(
70 Effect.gen(function* () {
71 const { dbPath } = yield* ServerConfig.ServerConfig;
72 yield* initializeV2Database(dbPath);
73 return layerFromPath(dbPath);
74 }),
75);