1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";
3import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as Scope from "effect/Scope";
8import * as SqlClient from "effect/sql/SqlClient";
9
10import { snapshotCookieDatabase } from "./CookieDatabase.ts";
11
12const runNode = <A, E>(
13 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | Scope.Scope>,
14) => effect.pipe(Effect.provide(NodeServices.layer), Effect.scoped);
15
16describe("snapshotCookieDatabase", () => {
17 it.effect("includes committed WAL data in one consistent database", () =>
18 runNode(
19 Effect.gen(function* () {
20 const fileSystem = yield* FileSystem.FileSystem;
21 const path = yield* Path.Path;
22 const sourceDirectory = yield* fileSystem.makeTempDirectoryScoped({
23 prefix: "t3code-cookie-source-",
24 });
25 const source = path.join(sourceDirectory, "Cookies");
26 const snapshot = yield* Effect.gen(function* () {
27 const sql = yield* SqlClient.SqlClient;
28 yield* sql`PRAGMA journal_mode = WAL`;
29 yield* sql`PRAGMA wal_autocheckpoint = 0`;
30 yield* sql`CREATE TABLE cookies(name TEXT NOT NULL)`;
31 yield* sql`INSERT INTO cookies(name) VALUES (${"committed-in-wal"})`;
32 expect(yield* fileSystem.exists(`${source}-wal`)).toBe(true);
33 return yield* snapshotCookieDatabase(source);
34 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: source })));
35 const rows = yield* Effect.gen(function* () {
36 const sql = yield* SqlClient.SqlClient;
37 return yield* sql<{ readonly name: string }>`SELECT name FROM cookies`;
38 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: snapshot, readonly: true })));
39 expect(rows).toEqual([{ name: "committed-in-wal" }]);
40 }),
41 ),
42 );
43
44 it.effect("propagates snapshot failures and removes its temporary directory", () =>
45 runNode(
46 Effect.gen(function* () {
47 const fileSystem = yield* FileSystem.FileSystem;
48 const path = yield* Path.Path;
49 const sourceDirectory = yield* fileSystem.makeTempDirectoryScoped({
50 prefix: "t3code-cookie-invalid-source-",
51 });
52 const source = path.join(sourceDirectory, "Cookies");
53 yield* fileSystem.writeFileString(source, "not a sqlite database");
54 const prefix = `t3code-cookie-failed-${process.pid}-`;
55 const error = yield* snapshotCookieDatabase(source, prefix).pipe(
56 Effect.scoped,
57 Effect.flip,
58 );
59 expect(error._tag).toBe("SqlError");
60 const temporaryEntries = yield* fileSystem.readDirectory(path.dirname(sourceDirectory));
61 expect(temporaryEntries.some((entry) => entry.startsWith(prefix))).toBe(false);
62 }),
63 ),
64 );
65
66 it.effect("removes a successful snapshot when its scope closes", () =>
67 runNode(
68 Effect.gen(function* () {
69 const fileSystem = yield* FileSystem.FileSystem;
70 const path = yield* Path.Path;
71 const sourceDirectory = yield* fileSystem.makeTempDirectoryScoped({
72 prefix: "t3code-cookie-cleanup-source-",
73 });
74 const source = path.join(sourceDirectory, "Cookies");
75 yield* Effect.gen(function* () {
76 const sql = yield* SqlClient.SqlClient;
77 yield* sql`CREATE TABLE cookies(name TEXT NOT NULL)`;
78 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: source })));
79 const snapshot = yield* snapshotCookieDatabase(source).pipe(Effect.scoped);
80 expect(yield* fileSystem.exists(snapshot)).toBe(false);
81 }),
82 ),
83 );
84});