@tsdoctor/snapshot 0.1.0 → 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 +4 -3
- package/SnapshotService.js +158 -3
- package/index.d.ts +46 -28
- package/index.js +1 -2
- package/package.json +2 -2
- package/SnapshotServiceLive.js +0 -131
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
|
-
- **`
|
|
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,
|
|
31
|
+
import { SnapshotService, hashContent } from "@tsdoctor/snapshot";
|
|
31
32
|
import { Effect } from "effect";
|
|
32
33
|
|
|
33
|
-
const layer =
|
|
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;
|
package/SnapshotService.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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,34 +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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
174
192
|
//#endregion
|
|
175
|
-
export { type FileSnapshot, SnapshotDbError, SnapshotService,
|
|
193
|
+
export { type FileSnapshot, SnapshotDbError, SnapshotService, type SnapshotServiceShape, hashContent, hashFrontmatter, normalizeContent };
|
|
176
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,
|
|
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.
|
|
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": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@effected/store": "^0.
|
|
40
|
+
"@effected/store": "^0.5.0",
|
|
41
41
|
"effect": "4.0.0-rc.109"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
package/SnapshotServiceLive.js
DELETED
|
@@ -1,131 +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. A WAL checkpoint (`PRAGMA wal_checkpoint(TRUNCATE)`) is
|
|
53
|
-
* registered as a scope finalizer so the sidecar files are folded back into
|
|
54
|
-
* the main database on clean shutdown.
|
|
55
|
-
*
|
|
56
|
-
* This is a parameterized layer factory: call it once per database path and
|
|
57
|
-
* bind the result to a `const` — layers memoize by reference, and a fresh
|
|
58
|
-
* call at each provide site would open the database more than once. The
|
|
59
|
-
* parent directory of `dbPath` must already exist.
|
|
60
|
-
*
|
|
61
|
-
* @param dbPath - Path to the SQLite database file
|
|
62
|
-
* @returns A layer providing {@link SnapshotService}
|
|
63
|
-
* @public
|
|
64
|
-
*/
|
|
65
|
-
const SnapshotServiceLive = (dbPath) => {
|
|
66
|
-
const StoreLive = Store.layerSqlite({
|
|
67
|
-
filename: dbPath,
|
|
68
|
-
migrations
|
|
69
|
-
});
|
|
70
|
-
const ServiceImpl = Layer.effect(SnapshotService, Effect.gen(function* () {
|
|
71
|
-
const sql = (yield* Store).client;
|
|
72
|
-
yield* Effect.addFinalizer(() => sql`PRAGMA wal_checkpoint(TRUNCATE)`.pipe(Effect.ignore));
|
|
73
|
-
return {
|
|
74
|
-
hashContent,
|
|
75
|
-
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)),
|
|
76
|
-
getAllForDirectory: (outputDir) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map(toFileSnapshot)), Effect.mapError(toSnapshotDbError)),
|
|
77
|
-
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)),
|
|
78
|
-
upsert: (snapshot) => sql`INSERT INTO file_snapshots
|
|
79
|
-
(output_dir, file_path, published_time, modified_time,
|
|
80
|
-
content_hash, frontmatter_hash, build_time)
|
|
81
|
-
VALUES (${snapshot.outputDir}, ${snapshot.filePath},
|
|
82
|
-
${snapshot.publishedTime}, ${snapshot.modifiedTime},
|
|
83
|
-
${snapshot.contentHash}, ${snapshot.frontmatterHash},
|
|
84
|
-
${snapshot.buildTime})
|
|
85
|
-
ON CONFLICT(output_dir, file_path) DO UPDATE SET
|
|
86
|
-
published_time = ${snapshot.publishedTime},
|
|
87
|
-
modified_time = ${snapshot.modifiedTime},
|
|
88
|
-
content_hash = ${snapshot.contentHash},
|
|
89
|
-
frontmatter_hash = ${snapshot.frontmatterHash},
|
|
90
|
-
build_time = ${snapshot.buildTime}
|
|
91
|
-
WHERE published_time != ${snapshot.publishedTime}
|
|
92
|
-
OR modified_time != ${snapshot.modifiedTime}
|
|
93
|
-
OR content_hash != ${snapshot.contentHash}
|
|
94
|
-
OR frontmatter_hash != ${snapshot.frontmatterHash}`.pipe(Effect.as(true), Effect.mapError(toSnapshotDbError)),
|
|
95
|
-
batchUpsert: (snapshots) => (snapshots.length === 0 ? Effect.succeed(0) : sql.withTransaction(Effect.forEach(snapshots, (s) => sql`INSERT INTO file_snapshots
|
|
96
|
-
(output_dir, file_path, published_time, modified_time,
|
|
97
|
-
content_hash, frontmatter_hash, build_time)
|
|
98
|
-
VALUES (${s.outputDir}, ${s.filePath},
|
|
99
|
-
${s.publishedTime}, ${s.modifiedTime},
|
|
100
|
-
${s.contentHash}, ${s.frontmatterHash},
|
|
101
|
-
${s.buildTime})
|
|
102
|
-
ON CONFLICT(output_dir, file_path) DO UPDATE SET
|
|
103
|
-
published_time = ${s.publishedTime},
|
|
104
|
-
modified_time = ${s.modifiedTime},
|
|
105
|
-
content_hash = ${s.contentHash},
|
|
106
|
-
frontmatter_hash = ${s.frontmatterHash},
|
|
107
|
-
build_time = ${s.buildTime}
|
|
108
|
-
WHERE published_time != ${s.publishedTime}
|
|
109
|
-
OR modified_time != ${s.modifiedTime}
|
|
110
|
-
OR content_hash != ${s.contentHash}
|
|
111
|
-
OR frontmatter_hash != ${s.frontmatterHash}`, { concurrency: 1 })).pipe(Effect.map(() => snapshots.length))).pipe(Effect.mapError(toSnapshotDbError)),
|
|
112
|
-
deleteSnapshot: (outputDir, filePath) => sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.asVoid, Effect.mapError(toSnapshotDbError)),
|
|
113
|
-
cleanupStale: (outputDir, currentFiles) => Effect.gen(function* () {
|
|
114
|
-
const rows = yield* sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`;
|
|
115
|
-
const staleFiles = [];
|
|
116
|
-
for (const row of rows) {
|
|
117
|
-
const fp = row.file_path;
|
|
118
|
-
if (!currentFiles.has(fp)) {
|
|
119
|
-
yield* sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${fp}`;
|
|
120
|
-
staleFiles.push(fp);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return staleFiles;
|
|
124
|
-
}).pipe(Effect.mapError(toSnapshotDbError))
|
|
125
|
-
};
|
|
126
|
-
}));
|
|
127
|
-
return Layer.provide(ServiceImpl, StoreLive);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
//#endregion
|
|
131
|
-
export { SnapshotServiceLive };
|