@tsdoctor/snapshot 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,8 @@ Incremental-build snapshot tracking for static documentation pipelines. The pack
10
10
  ## What you get
11
11
 
12
12
  - **`SnapshotService`** — an Effect service tag with typed operations: single and bulk lookup, transactional batch upsert (with a conditional `ON CONFLICT ... DO UPDATE ... WHERE` that avoids rewriting unchanged rows), deletion and stale-entry cleanup.
13
- - **`SnapshotServiceLive(dbPath)`** — the live layer over a SQLite file. Layer construction applies migrations through `@effected/store`'s ledger; a WAL checkpoint runs as a scope finalizer on clean shutdown.
13
+ - **`SnapshotService.layer(dbPath)`** — the live layer over a SQLite file. Layer construction applies migrations through `@effected/store`'s ledger; a WAL checkpoint runs as a scope finalizer on clean shutdown. It is a factory: call it once per database path and bind the result to a `const`, since layers memoize by reference.
14
+ - **`SnapshotService.makeTest(overrides)` / `SnapshotService.layerTest(overrides)`** — an in-memory double describing a build with no prior snapshot: every lookup misses, every write is accepted and discarded, and nothing is reported stale.
14
15
  - **`hashContent` / `hashFrontmatter` / `normalizeContent`** — pure SHA-256 helpers that normalize markdown bodies and frontmatter (excluding timestamp fields) into stable change-detection hashes.
15
16
  - **`FileSnapshot`** / **`SnapshotDbError`** — the tracked-file record and the typed error every operation can fail with.
16
17
 
@@ -27,10 +28,10 @@ This is an ESM-only package. `effect` (v4) and `@effected/store` are peer depend
27
28
  ## Quick start
28
29
 
29
30
  ```ts
30
- import { SnapshotService, SnapshotServiceLive, hashContent } from "@tsdoctor/snapshot";
31
+ import { SnapshotService, hashContent } from "@tsdoctor/snapshot";
31
32
  import { Effect } from "effect";
32
33
 
33
- const layer = SnapshotServiceLive(".api-docs/snapshot/api-docs.db");
34
+ const layer = SnapshotService.layer(".api-docs/snapshot/api-docs.db");
34
35
 
35
36
  const program = Effect.gen(function* () {
36
37
  const snapshots = yield* SnapshotService;
@@ -1,4 +1,6 @@
1
- import { Context, Data } from "effect";
1
+ import { hashContent } from "./content-hash.js";
2
+ import { Store } from "@effected/store";
3
+ import { Context, Data, Effect, Layer, Option } from "effect";
2
4
 
3
5
  //#region src/SnapshotService.ts
4
6
  /**
@@ -16,12 +18,165 @@ var SnapshotDbError = class extends Data.TaggedError("SnapshotDbError") {
16
18
  * Effect service tag for the snapshot tracking store.
17
19
  *
18
20
  * @remarks
19
- * Provided by {@link SnapshotServiceLive}, which backs it with a
21
+ * Provided by {@link SnapshotService.layer}, which backs it with a
20
22
  * schema-versioned SQLite database via `@effected/store`.
21
23
  *
22
24
  * @public
23
25
  */
24
- var SnapshotService = class extends Context.Service()("@tsdoctor/snapshot/SnapshotService") {};
26
+ var SnapshotService = class SnapshotService extends Context.Service()("@tsdoctor/snapshot/SnapshotService") {
27
+ /**
28
+ * Builds the live {@link SnapshotService} layer over a SQLite database file.
29
+ *
30
+ * @remarks
31
+ * Backed by `@effected/store`'s `Store.layerSqlite`: layer construction opens
32
+ * the database (WAL mode), ensures the migration ledger and applies pending
33
+ * migrations. `checkpointOnClose` folds the WAL sidecar files back into the
34
+ * main database on clean shutdown.
35
+ *
36
+ * This is a parameterized layer factory: call it once per database path and
37
+ * bind the result to a `const` — layers memoize by reference, and a fresh
38
+ * call at each provide site would open the database more than once. The
39
+ * parent directory of `dbPath` must already exist.
40
+ *
41
+ * @param dbPath - Path to the SQLite database file
42
+ * @returns A layer providing {@link SnapshotService}
43
+ * @public
44
+ */
45
+ static layer = (dbPath) => make(dbPath);
46
+ /**
47
+ * An in-memory double: no SQLite file, no migrations, no WAL.
48
+ *
49
+ * @remarks
50
+ * Defaults describe a build with **no prior snapshot** — every lookup misses
51
+ * and every write is accepted and discarded, which is the state a first
52
+ * build or a fresh clone is in, and the state the disk-fallback path is
53
+ * written against.
54
+ *
55
+ * `cleanupStale` defaults to reporting nothing stale rather than echoing its
56
+ * input: a double that claimed files were stale would have the caller DELETE
57
+ * them from disk.
58
+ *
59
+ * @public
60
+ */
61
+ static makeTest = (overrides = {}) => ({
62
+ getSnapshot: overrides.getSnapshot ?? (() => Effect.succeed(Option.none())),
63
+ getAllForDirectory: overrides.getAllForDirectory ?? (() => Effect.succeed([])),
64
+ getFilePaths: overrides.getFilePaths ?? (() => Effect.succeed([])),
65
+ upsert: overrides.upsert ?? (() => Effect.succeed(true)),
66
+ batchUpsert: overrides.batchUpsert ?? ((snapshots) => Effect.succeed(snapshots.length)),
67
+ deleteSnapshot: overrides.deleteSnapshot ?? (() => Effect.void),
68
+ cleanupStale: overrides.cleanupStale ?? (() => Effect.succeed([]))
69
+ });
70
+ /**
71
+ * {@link SnapshotService.makeTest} behind a `Layer`.
72
+ *
73
+ * @public
74
+ */
75
+ static layerTest = (overrides = {}) => Layer.succeed(SnapshotService, SnapshotService.makeTest(overrides));
76
+ };
77
+ function toFileSnapshot(row) {
78
+ return {
79
+ outputDir: row.output_dir,
80
+ filePath: row.file_path,
81
+ publishedTime: row.published_time,
82
+ modifiedTime: row.modified_time,
83
+ contentHash: row.content_hash,
84
+ frontmatterHash: row.frontmatter_hash,
85
+ buildTime: row.build_time
86
+ };
87
+ }
88
+ function toSnapshotDbError(error) {
89
+ return new SnapshotDbError({
90
+ operation: "query",
91
+ dbPath: "snapshot-db",
92
+ reason: error instanceof Error ? error.message : String(error)
93
+ });
94
+ }
95
+ const migrations = [{
96
+ id: 1,
97
+ name: "001_create_snapshots",
98
+ up: (sql) => Effect.gen(function* () {
99
+ yield* sql`
100
+ CREATE TABLE IF NOT EXISTS file_snapshots (
101
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
102
+ output_dir TEXT NOT NULL,
103
+ file_path TEXT NOT NULL,
104
+ published_time TEXT NOT NULL,
105
+ modified_time TEXT NOT NULL,
106
+ content_hash TEXT NOT NULL,
107
+ frontmatter_hash TEXT NOT NULL,
108
+ build_time TEXT NOT NULL,
109
+ UNIQUE(output_dir, file_path)
110
+ )
111
+ `;
112
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_output_dir ON file_snapshots(output_dir)`;
113
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_file_path ON file_snapshots(file_path)`;
114
+ })
115
+ }];
116
+ const make = (dbPath) => {
117
+ const StoreLive = Store.layerSqlite({
118
+ filename: dbPath,
119
+ migrations,
120
+ checkpointOnClose: true
121
+ });
122
+ const ServiceImpl = Layer.effect(SnapshotService, Effect.gen(function* () {
123
+ const sql = (yield* Store).client;
124
+ return {
125
+ hashContent,
126
+ getSnapshot: (outputDir, filePath) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.map((rows) => rows.length > 0 ? Option.some(toFileSnapshot(rows[0])) : Option.none()), Effect.mapError(toSnapshotDbError)),
127
+ getAllForDirectory: (outputDir) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map(toFileSnapshot)), Effect.mapError(toSnapshotDbError)),
128
+ getFilePaths: (outputDir) => sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map((r) => r.file_path)), Effect.mapError(toSnapshotDbError)),
129
+ upsert: (snapshot) => sql`INSERT INTO file_snapshots
130
+ (output_dir, file_path, published_time, modified_time,
131
+ content_hash, frontmatter_hash, build_time)
132
+ VALUES (${snapshot.outputDir}, ${snapshot.filePath},
133
+ ${snapshot.publishedTime}, ${snapshot.modifiedTime},
134
+ ${snapshot.contentHash}, ${snapshot.frontmatterHash},
135
+ ${snapshot.buildTime})
136
+ ON CONFLICT(output_dir, file_path) DO UPDATE SET
137
+ published_time = ${snapshot.publishedTime},
138
+ modified_time = ${snapshot.modifiedTime},
139
+ content_hash = ${snapshot.contentHash},
140
+ frontmatter_hash = ${snapshot.frontmatterHash},
141
+ build_time = ${snapshot.buildTime}
142
+ WHERE published_time != ${snapshot.publishedTime}
143
+ OR modified_time != ${snapshot.modifiedTime}
144
+ OR content_hash != ${snapshot.contentHash}
145
+ OR frontmatter_hash != ${snapshot.frontmatterHash}`.pipe(Effect.as(true), Effect.mapError(toSnapshotDbError)),
146
+ batchUpsert: (snapshots) => (snapshots.length === 0 ? Effect.succeed(0) : sql.withTransaction(Effect.forEach(snapshots, (s) => sql`INSERT INTO file_snapshots
147
+ (output_dir, file_path, published_time, modified_time,
148
+ content_hash, frontmatter_hash, build_time)
149
+ VALUES (${s.outputDir}, ${s.filePath},
150
+ ${s.publishedTime}, ${s.modifiedTime},
151
+ ${s.contentHash}, ${s.frontmatterHash},
152
+ ${s.buildTime})
153
+ ON CONFLICT(output_dir, file_path) DO UPDATE SET
154
+ published_time = ${s.publishedTime},
155
+ modified_time = ${s.modifiedTime},
156
+ content_hash = ${s.contentHash},
157
+ frontmatter_hash = ${s.frontmatterHash},
158
+ build_time = ${s.buildTime}
159
+ WHERE published_time != ${s.publishedTime}
160
+ OR modified_time != ${s.modifiedTime}
161
+ OR content_hash != ${s.contentHash}
162
+ OR frontmatter_hash != ${s.frontmatterHash}`, { concurrency: 1 })).pipe(Effect.map(() => snapshots.length))).pipe(Effect.mapError(toSnapshotDbError)),
163
+ deleteSnapshot: (outputDir, filePath) => sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.asVoid, Effect.mapError(toSnapshotDbError)),
164
+ cleanupStale: (outputDir, currentFiles) => Effect.gen(function* () {
165
+ const rows = yield* sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`;
166
+ const staleFiles = [];
167
+ for (const row of rows) {
168
+ const fp = row.file_path;
169
+ if (!currentFiles.has(fp)) {
170
+ yield* sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${fp}`;
171
+ staleFiles.push(fp);
172
+ }
173
+ }
174
+ return staleFiles;
175
+ }).pipe(Effect.mapError(toSnapshotDbError))
176
+ };
177
+ }));
178
+ return Layer.provide(ServiceImpl, StoreLive);
179
+ };
25
180
 
26
181
  //#endregion
27
182
  export { SnapshotDbError, SnapshotService };
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Context, Effect, Layer, Option } from "effect";
2
1
  import { StoreError, StoreMigrationError } from "@effected/store";
2
+ import { Context, Effect, Layer, Option } from "effect";
3
3
  //#region src/content-hash.d.ts
4
4
  /**
5
5
  * Normalizes content string for consistent hashing.
@@ -119,8 +119,6 @@ declare class SnapshotDbError extends SnapshotDbError_base<{
119
119
  * @public
120
120
  */
121
121
  interface SnapshotServiceShape {
122
- /** Hash normalized content with SHA-256 (pure, synchronous). */
123
- readonly hashContent: (content: string) => string;
124
122
  /** Look up a single snapshot by output directory and file path. */
125
123
  readonly getSnapshot: (outputDir: string, filePath: string) => Effect.Effect<Option.Option<FileSnapshot>, SnapshotDbError>;
126
124
  /** Load every snapshot recorded for an output directory. */
@@ -143,33 +141,54 @@ declare const SnapshotService_base: Context.ServiceClass<SnapshotService, "@tsdo
143
141
  * Effect service tag for the snapshot tracking store.
144
142
  *
145
143
  * @remarks
146
- * Provided by {@link SnapshotServiceLive}, which backs it with a
144
+ * Provided by {@link SnapshotService.layer}, which backs it with a
147
145
  * schema-versioned SQLite database via `@effected/store`.
148
146
  *
149
147
  * @public
150
148
  */
151
- declare class SnapshotService extends SnapshotService_base {}
152
- //#endregion
153
- //#region src/SnapshotServiceLive.d.ts
154
- /**
155
- * Builds the live {@link SnapshotService} layer over a SQLite database file.
156
- *
157
- * @remarks
158
- * Backed by `@effected/store`'s `Store.layerSqlite`: layer construction opens
159
- * the database (WAL mode), ensures the migration ledger and applies pending
160
- * migrations. `checkpointOnClose` folds the WAL sidecar files back into the
161
- * main database on clean shutdown.
162
- *
163
- * This is a parameterized layer factory: call it once per database path and
164
- * bind the result to a `const` layers memoize by reference, and a fresh
165
- * call at each provide site would open the database more than once. The
166
- * parent directory of `dbPath` must already exist.
167
- *
168
- * @param dbPath - Path to the SQLite database file
169
- * @returns A layer providing {@link SnapshotService}
170
- * @public
171
- */
172
- declare const SnapshotServiceLive: (dbPath: string) => Layer.Layer<SnapshotService, StoreError | StoreMigrationError>;
149
+ declare class SnapshotService extends SnapshotService_base {
150
+ /**
151
+ * Builds the live {@link SnapshotService} layer over a SQLite database file.
152
+ *
153
+ * @remarks
154
+ * Backed by `@effected/store`'s `Store.layerSqlite`: layer construction opens
155
+ * the database (WAL mode), ensures the migration ledger and applies pending
156
+ * migrations. `checkpointOnClose` folds the WAL sidecar files back into the
157
+ * main database on clean shutdown.
158
+ *
159
+ * This is a parameterized layer factory: call it once per database path and
160
+ * bind the result to a `const` — layers memoize by reference, and a fresh
161
+ * call at each provide site would open the database more than once. The
162
+ * parent directory of `dbPath` must already exist.
163
+ *
164
+ * @param dbPath - Path to the SQLite database file
165
+ * @returns A layer providing {@link SnapshotService}
166
+ * @public
167
+ */
168
+ static readonly layer: (dbPath: string) => Layer.Layer<SnapshotService, StoreError | StoreMigrationError>;
169
+ /**
170
+ * An in-memory double: no SQLite file, no migrations, no WAL.
171
+ *
172
+ * @remarks
173
+ * Defaults describe a build with **no prior snapshot** — every lookup misses
174
+ * and every write is accepted and discarded, which is the state a first
175
+ * build or a fresh clone is in, and the state the disk-fallback path is
176
+ * written against.
177
+ *
178
+ * `cleanupStale` defaults to reporting nothing stale rather than echoing its
179
+ * input: a double that claimed files were stale would have the caller DELETE
180
+ * them from disk.
181
+ *
182
+ * @public
183
+ */
184
+ static readonly makeTest: (overrides?: Partial<SnapshotServiceShape>) => SnapshotServiceShape;
185
+ /**
186
+ * {@link SnapshotService.makeTest} behind a `Layer`.
187
+ *
188
+ * @public
189
+ */
190
+ static readonly layerTest: (overrides?: Partial<SnapshotServiceShape>) => Layer.Layer<SnapshotService>;
191
+ }
173
192
  //#endregion
174
- export { type FileSnapshot, SnapshotDbError, SnapshotService, SnapshotServiceLive, type SnapshotServiceShape, hashContent, hashFrontmatter, normalizeContent };
193
+ export { type FileSnapshot, SnapshotDbError, SnapshotService, type SnapshotServiceShape, hashContent, hashFrontmatter, normalizeContent };
175
194
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { hashContent, hashFrontmatter, normalizeContent } from "./content-hash.js";
2
2
  import { SnapshotDbError, SnapshotService } from "./SnapshotService.js";
3
- import { SnapshotServiceLive } from "./SnapshotServiceLive.js";
4
3
 
5
- export { SnapshotDbError, SnapshotService, SnapshotServiceLive, hashContent, hashFrontmatter, normalizeContent };
4
+ export { SnapshotDbError, SnapshotService, hashContent, hashFrontmatter, normalizeContent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsdoctor/snapshot",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Incremental-build snapshot tracking for static documentation pipelines: a schema-versioned SQLite store of per-file content hashes and timestamps, built on @effected/store, plus the pure SHA-256 content-hashing helpers that feed it.",
6
6
  "keywords": [
@@ -1,130 +0,0 @@
1
- import { hashContent } from "./content-hash.js";
2
- import { SnapshotDbError, SnapshotService } from "./SnapshotService.js";
3
- import { Effect, Layer, Option } from "effect";
4
- import { Store } from "@effected/store";
5
-
6
- //#region src/SnapshotServiceLive.ts
7
- function toFileSnapshot(row) {
8
- return {
9
- outputDir: row.output_dir,
10
- filePath: row.file_path,
11
- publishedTime: row.published_time,
12
- modifiedTime: row.modified_time,
13
- contentHash: row.content_hash,
14
- frontmatterHash: row.frontmatter_hash,
15
- buildTime: row.build_time
16
- };
17
- }
18
- function toSnapshotDbError(error) {
19
- return new SnapshotDbError({
20
- operation: "query",
21
- dbPath: "snapshot-db",
22
- reason: error instanceof Error ? error.message : String(error)
23
- });
24
- }
25
- const migrations = [{
26
- id: 1,
27
- name: "001_create_snapshots",
28
- up: (sql) => Effect.gen(function* () {
29
- yield* sql`
30
- CREATE TABLE IF NOT EXISTS file_snapshots (
31
- id INTEGER PRIMARY KEY AUTOINCREMENT,
32
- output_dir TEXT NOT NULL,
33
- file_path TEXT NOT NULL,
34
- published_time TEXT NOT NULL,
35
- modified_time TEXT NOT NULL,
36
- content_hash TEXT NOT NULL,
37
- frontmatter_hash TEXT NOT NULL,
38
- build_time TEXT NOT NULL,
39
- UNIQUE(output_dir, file_path)
40
- )
41
- `;
42
- yield* sql`CREATE INDEX IF NOT EXISTS idx_output_dir ON file_snapshots(output_dir)`;
43
- yield* sql`CREATE INDEX IF NOT EXISTS idx_file_path ON file_snapshots(file_path)`;
44
- })
45
- }];
46
- /**
47
- * Builds the live {@link SnapshotService} layer over a SQLite database file.
48
- *
49
- * @remarks
50
- * Backed by `@effected/store`'s `Store.layerSqlite`: layer construction opens
51
- * the database (WAL mode), ensures the migration ledger and applies pending
52
- * migrations. `checkpointOnClose` folds the WAL sidecar files back into the
53
- * main database on clean shutdown.
54
- *
55
- * This is a parameterized layer factory: call it once per database path and
56
- * bind the result to a `const` — layers memoize by reference, and a fresh
57
- * call at each provide site would open the database more than once. The
58
- * parent directory of `dbPath` must already exist.
59
- *
60
- * @param dbPath - Path to the SQLite database file
61
- * @returns A layer providing {@link SnapshotService}
62
- * @public
63
- */
64
- const SnapshotServiceLive = (dbPath) => {
65
- const StoreLive = Store.layerSqlite({
66
- filename: dbPath,
67
- migrations,
68
- checkpointOnClose: true
69
- });
70
- const ServiceImpl = Layer.effect(SnapshotService, Effect.gen(function* () {
71
- const sql = (yield* Store).client;
72
- return {
73
- hashContent,
74
- getSnapshot: (outputDir, filePath) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.map((rows) => rows.length > 0 ? Option.some(toFileSnapshot(rows[0])) : Option.none()), Effect.mapError(toSnapshotDbError)),
75
- getAllForDirectory: (outputDir) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map(toFileSnapshot)), Effect.mapError(toSnapshotDbError)),
76
- getFilePaths: (outputDir) => sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map((r) => r.file_path)), Effect.mapError(toSnapshotDbError)),
77
- upsert: (snapshot) => sql`INSERT INTO file_snapshots
78
- (output_dir, file_path, published_time, modified_time,
79
- content_hash, frontmatter_hash, build_time)
80
- VALUES (${snapshot.outputDir}, ${snapshot.filePath},
81
- ${snapshot.publishedTime}, ${snapshot.modifiedTime},
82
- ${snapshot.contentHash}, ${snapshot.frontmatterHash},
83
- ${snapshot.buildTime})
84
- ON CONFLICT(output_dir, file_path) DO UPDATE SET
85
- published_time = ${snapshot.publishedTime},
86
- modified_time = ${snapshot.modifiedTime},
87
- content_hash = ${snapshot.contentHash},
88
- frontmatter_hash = ${snapshot.frontmatterHash},
89
- build_time = ${snapshot.buildTime}
90
- WHERE published_time != ${snapshot.publishedTime}
91
- OR modified_time != ${snapshot.modifiedTime}
92
- OR content_hash != ${snapshot.contentHash}
93
- OR frontmatter_hash != ${snapshot.frontmatterHash}`.pipe(Effect.as(true), Effect.mapError(toSnapshotDbError)),
94
- batchUpsert: (snapshots) => (snapshots.length === 0 ? Effect.succeed(0) : sql.withTransaction(Effect.forEach(snapshots, (s) => sql`INSERT INTO file_snapshots
95
- (output_dir, file_path, published_time, modified_time,
96
- content_hash, frontmatter_hash, build_time)
97
- VALUES (${s.outputDir}, ${s.filePath},
98
- ${s.publishedTime}, ${s.modifiedTime},
99
- ${s.contentHash}, ${s.frontmatterHash},
100
- ${s.buildTime})
101
- ON CONFLICT(output_dir, file_path) DO UPDATE SET
102
- published_time = ${s.publishedTime},
103
- modified_time = ${s.modifiedTime},
104
- content_hash = ${s.contentHash},
105
- frontmatter_hash = ${s.frontmatterHash},
106
- build_time = ${s.buildTime}
107
- WHERE published_time != ${s.publishedTime}
108
- OR modified_time != ${s.modifiedTime}
109
- OR content_hash != ${s.contentHash}
110
- OR frontmatter_hash != ${s.frontmatterHash}`, { concurrency: 1 })).pipe(Effect.map(() => snapshots.length))).pipe(Effect.mapError(toSnapshotDbError)),
111
- deleteSnapshot: (outputDir, filePath) => sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.asVoid, Effect.mapError(toSnapshotDbError)),
112
- cleanupStale: (outputDir, currentFiles) => Effect.gen(function* () {
113
- const rows = yield* sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`;
114
- const staleFiles = [];
115
- for (const row of rows) {
116
- const fp = row.file_path;
117
- if (!currentFiles.has(fp)) {
118
- yield* sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${fp}`;
119
- staleFiles.push(fp);
120
- }
121
- }
122
- return staleFiles;
123
- }).pipe(Effect.mapError(toSnapshotDbError))
124
- };
125
- }));
126
- return Layer.provide(ServiceImpl, StoreLive);
127
- };
128
-
129
- //#endregion
130
- export { SnapshotServiceLive };