@gmickel/gno 1.19.0 → 1.21.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 +28 -8
- package/assets/skill/SKILL.md +73 -27
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +1 -1
- package/spec/cli.md +142 -17
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +87 -5
- package/spec/mcp.md +75 -3
- package/spec/output-schemas/ask.schema.json +198 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +4 -0
- package/src/app/context-runtime.ts +5 -1
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +40 -149
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +227 -1
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +33 -21
- package/src/core/context-evidence.ts +6 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/core/retrieval-trace-evidence-origin.ts +3 -0
- package/src/core/retrieval-trace-session.ts +15 -2
- package/src/ingestion/sync.ts +214 -165
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/context.ts +28 -7
- package/src/mcp/tools/index.ts +38 -0
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/types.ts +25 -0
- package/src/sdk/client.ts +77 -2
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +162 -3
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/** SQLite persistence for the bounded, metadata-only document change journal. */
|
|
2
|
+
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
DocumentChangeKind,
|
|
7
|
+
DocumentChangeListOptions,
|
|
8
|
+
DocumentChangePage,
|
|
9
|
+
DocumentChangePurgeResult,
|
|
10
|
+
DocumentChangeRetentionPolicy,
|
|
11
|
+
DocumentChangeRetentionResult,
|
|
12
|
+
DocumentChangeRow,
|
|
13
|
+
DocumentChangeStructureDelta,
|
|
14
|
+
DocumentRow,
|
|
15
|
+
StoreResult,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
decodeDocumentChangeCursor,
|
|
20
|
+
DEFAULT_DOCUMENT_CHANGE_RETENTION,
|
|
21
|
+
encodeDocumentChangeCursor,
|
|
22
|
+
serializeDocumentChangeStructureDelta,
|
|
23
|
+
validateDocumentChangeRetentionPolicy,
|
|
24
|
+
} from "../../core/change-journal";
|
|
25
|
+
import { err, ok } from "../types";
|
|
26
|
+
|
|
27
|
+
const DAY_MS = 86_400_000;
|
|
28
|
+
const MAX_PAGE_SIZE = 1_000;
|
|
29
|
+
const RETENTION_DELETE_SCAN_BATCH_SIZE = 256;
|
|
30
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
31
|
+
|
|
32
|
+
export interface DocumentChangeSnapshot {
|
|
33
|
+
id: number;
|
|
34
|
+
collection: string;
|
|
35
|
+
relPath: string;
|
|
36
|
+
docid: string;
|
|
37
|
+
uri: string;
|
|
38
|
+
sourceHash: string;
|
|
39
|
+
mirrorHash: string | null;
|
|
40
|
+
active: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DocumentChangeDraft {
|
|
44
|
+
documentId: number;
|
|
45
|
+
collection: string;
|
|
46
|
+
kind: DocumentChangeKind;
|
|
47
|
+
oldSnapshot: DocumentChangeSnapshot | null;
|
|
48
|
+
newSnapshot: DocumentChangeSnapshot | null;
|
|
49
|
+
structureDelta?: Partial<DocumentChangeStructureDelta>;
|
|
50
|
+
observedAtMs: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface DbDocumentChangeRow {
|
|
54
|
+
sequence: number;
|
|
55
|
+
document_id: number;
|
|
56
|
+
collection: string;
|
|
57
|
+
change_kind: DocumentChangeKind;
|
|
58
|
+
old_rel_path: string | null;
|
|
59
|
+
new_rel_path: string | null;
|
|
60
|
+
old_docid: string | null;
|
|
61
|
+
new_docid: string | null;
|
|
62
|
+
old_uri: string | null;
|
|
63
|
+
new_uri: string | null;
|
|
64
|
+
old_source_hash: string | null;
|
|
65
|
+
new_source_hash: string | null;
|
|
66
|
+
old_mirror_hash: string | null;
|
|
67
|
+
new_mirror_hash: string | null;
|
|
68
|
+
old_active: number | null;
|
|
69
|
+
new_active: number | null;
|
|
70
|
+
heading_delta_json: string;
|
|
71
|
+
link_delta_json: string;
|
|
72
|
+
typed_edge_delta_json: string;
|
|
73
|
+
date_delta_json: string;
|
|
74
|
+
structure_truncated: number;
|
|
75
|
+
observed_at_ms: number;
|
|
76
|
+
byte_size: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const utf8ByteLength = (value: string | null): number =>
|
|
80
|
+
value === null ? 0 : UTF8_ENCODER.encode(value).byteLength;
|
|
81
|
+
|
|
82
|
+
const byteSize = (
|
|
83
|
+
draft: DocumentChangeDraft,
|
|
84
|
+
deltaJson: readonly string[]
|
|
85
|
+
): number => {
|
|
86
|
+
const old = draft.oldSnapshot;
|
|
87
|
+
const next = draft.newSnapshot;
|
|
88
|
+
return (
|
|
89
|
+
32 +
|
|
90
|
+
utf8ByteLength(draft.collection) +
|
|
91
|
+
utf8ByteLength(draft.kind) +
|
|
92
|
+
utf8ByteLength(old?.relPath ?? null) +
|
|
93
|
+
utf8ByteLength(next?.relPath ?? null) +
|
|
94
|
+
utf8ByteLength(old?.docid ?? null) +
|
|
95
|
+
utf8ByteLength(next?.docid ?? null) +
|
|
96
|
+
utf8ByteLength(old?.uri ?? null) +
|
|
97
|
+
utf8ByteLength(next?.uri ?? null) +
|
|
98
|
+
utf8ByteLength(old?.sourceHash ?? null) +
|
|
99
|
+
utf8ByteLength(next?.sourceHash ?? null) +
|
|
100
|
+
utf8ByteLength(old?.mirrorHash ?? null) +
|
|
101
|
+
utf8ByteLength(next?.mirrorHash ?? null) +
|
|
102
|
+
deltaJson.reduce((total, value) => total + utf8ByteLength(value), 0)
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const mapRow = (row: DbDocumentChangeRow): DocumentChangeRow => ({
|
|
107
|
+
sequence: row.sequence,
|
|
108
|
+
documentId: row.document_id,
|
|
109
|
+
collection: row.collection,
|
|
110
|
+
kind: row.change_kind,
|
|
111
|
+
oldRelPath: row.old_rel_path,
|
|
112
|
+
newRelPath: row.new_rel_path,
|
|
113
|
+
oldDocid: row.old_docid,
|
|
114
|
+
newDocid: row.new_docid,
|
|
115
|
+
oldUri: row.old_uri,
|
|
116
|
+
newUri: row.new_uri,
|
|
117
|
+
oldSourceHash: row.old_source_hash,
|
|
118
|
+
newSourceHash: row.new_source_hash,
|
|
119
|
+
oldMirrorHash: row.old_mirror_hash,
|
|
120
|
+
newMirrorHash: row.new_mirror_hash,
|
|
121
|
+
oldActive: row.old_active === null ? null : row.old_active === 1,
|
|
122
|
+
newActive: row.new_active === null ? null : row.new_active === 1,
|
|
123
|
+
structureDelta: {
|
|
124
|
+
headings: JSON.parse(row.heading_delta_json),
|
|
125
|
+
links: JSON.parse(row.link_delta_json),
|
|
126
|
+
typedEdges: JSON.parse(row.typed_edge_delta_json),
|
|
127
|
+
dates: JSON.parse(row.date_delta_json),
|
|
128
|
+
truncated: row.structure_truncated === 1,
|
|
129
|
+
},
|
|
130
|
+
observedAtMs: row.observed_at_ms,
|
|
131
|
+
byteSize: row.byte_size,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const snapshotDocumentChange = (
|
|
135
|
+
row: DocumentRow
|
|
136
|
+
): DocumentChangeSnapshot => ({
|
|
137
|
+
id: row.id,
|
|
138
|
+
collection: row.collection,
|
|
139
|
+
relPath: row.relPath,
|
|
140
|
+
docid: row.docid,
|
|
141
|
+
uri: row.uri,
|
|
142
|
+
sourceHash: row.sourceHash,
|
|
143
|
+
mirrorHash: row.mirrorHash,
|
|
144
|
+
active: row.active,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const enforceInTransaction = (
|
|
148
|
+
db: Database,
|
|
149
|
+
policy: DocumentChangeRetentionPolicy,
|
|
150
|
+
nowMs: number
|
|
151
|
+
): DocumentChangeRetentionResult => {
|
|
152
|
+
validateDocumentChangeRetentionPolicy(policy, nowMs);
|
|
153
|
+
const state = db
|
|
154
|
+
.query<
|
|
155
|
+
{
|
|
156
|
+
retained_entries: number;
|
|
157
|
+
retained_bytes: number;
|
|
158
|
+
retention_floor: number;
|
|
159
|
+
},
|
|
160
|
+
[]
|
|
161
|
+
>(
|
|
162
|
+
`SELECT retained_entries, retained_bytes, retention_floor
|
|
163
|
+
FROM document_change_journal_state
|
|
164
|
+
WHERE singleton_id = 1`
|
|
165
|
+
)
|
|
166
|
+
.get() ?? {
|
|
167
|
+
retained_entries: 0,
|
|
168
|
+
retained_bytes: 0,
|
|
169
|
+
retention_floor: 0,
|
|
170
|
+
};
|
|
171
|
+
const ageBoundary = nowMs - policy.maxAgeDays * DAY_MS;
|
|
172
|
+
const ageCutoff =
|
|
173
|
+
db
|
|
174
|
+
.query<{ sequence: number | null }, [number]>(
|
|
175
|
+
`SELECT MAX(sequence) AS sequence
|
|
176
|
+
FROM document_changes
|
|
177
|
+
WHERE observed_at_ms <= ?`
|
|
178
|
+
)
|
|
179
|
+
.get(ageBoundary)?.sequence ?? state.retention_floor;
|
|
180
|
+
let deleted = 0;
|
|
181
|
+
let deletedBytes = 0;
|
|
182
|
+
let retentionFloor = state.retention_floor;
|
|
183
|
+
let afterSequence = state.retention_floor;
|
|
184
|
+
|
|
185
|
+
retentionScan: while (
|
|
186
|
+
state.retained_entries - deleted > policy.maxEntries ||
|
|
187
|
+
state.retained_bytes - deletedBytes > policy.maxBytes ||
|
|
188
|
+
afterSequence < ageCutoff
|
|
189
|
+
) {
|
|
190
|
+
const rows = db
|
|
191
|
+
.query<{ sequence: number; byte_size: number }, [number, number]>(
|
|
192
|
+
`SELECT sequence, byte_size
|
|
193
|
+
FROM document_changes
|
|
194
|
+
WHERE sequence > ?
|
|
195
|
+
ORDER BY sequence ASC
|
|
196
|
+
LIMIT ?`
|
|
197
|
+
)
|
|
198
|
+
.all(afterSequence, RETENTION_DELETE_SCAN_BATCH_SIZE);
|
|
199
|
+
if (rows.length === 0) break;
|
|
200
|
+
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
const overEntries = state.retained_entries - deleted > policy.maxEntries;
|
|
203
|
+
const overBytes = state.retained_bytes - deletedBytes > policy.maxBytes;
|
|
204
|
+
if (!(row.sequence <= ageCutoff || overEntries || overBytes)) {
|
|
205
|
+
break retentionScan;
|
|
206
|
+
}
|
|
207
|
+
deleted += 1;
|
|
208
|
+
deletedBytes += row.byte_size;
|
|
209
|
+
retentionFloor = row.sequence;
|
|
210
|
+
afterSequence = row.sequence;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (deleted > 0) {
|
|
215
|
+
db.run("DELETE FROM document_changes WHERE sequence <= ?", [
|
|
216
|
+
retentionFloor,
|
|
217
|
+
]);
|
|
218
|
+
db.run(
|
|
219
|
+
`UPDATE document_change_journal_state
|
|
220
|
+
SET retention_floor = MAX(retention_floor, ?),
|
|
221
|
+
retained_entries = retained_entries - ?,
|
|
222
|
+
retained_bytes = retained_bytes - ?
|
|
223
|
+
WHERE singleton_id = 1`,
|
|
224
|
+
[retentionFloor, deleted, deletedBytes]
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
deleted,
|
|
230
|
+
remainingEntries: state.retained_entries - deleted,
|
|
231
|
+
remainingBytes: state.retained_bytes - deletedBytes,
|
|
232
|
+
earliestCursor: encodeDocumentChangeCursor(retentionFloor),
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const appendDocumentChange = (
|
|
237
|
+
db: Database,
|
|
238
|
+
draft: DocumentChangeDraft
|
|
239
|
+
): void => {
|
|
240
|
+
if (!Number.isSafeInteger(draft.observedAtMs) || draft.observedAtMs < 0) {
|
|
241
|
+
throw new RangeError("Document change observedAtMs must be non-negative");
|
|
242
|
+
}
|
|
243
|
+
const {
|
|
244
|
+
delta,
|
|
245
|
+
headingDeltaJson,
|
|
246
|
+
linkDeltaJson,
|
|
247
|
+
typedEdgeDeltaJson,
|
|
248
|
+
dateDeltaJson,
|
|
249
|
+
} = serializeDocumentChangeStructureDelta(draft.structureDelta);
|
|
250
|
+
const old = draft.oldSnapshot;
|
|
251
|
+
const next = draft.newSnapshot;
|
|
252
|
+
const storedByteSize = byteSize(draft, [
|
|
253
|
+
headingDeltaJson,
|
|
254
|
+
linkDeltaJson,
|
|
255
|
+
typedEdgeDeltaJson,
|
|
256
|
+
dateDeltaJson,
|
|
257
|
+
]);
|
|
258
|
+
const inserted = db.run(
|
|
259
|
+
`INSERT INTO document_changes (
|
|
260
|
+
document_id, collection, change_kind,
|
|
261
|
+
old_rel_path, new_rel_path, old_docid, new_docid, old_uri, new_uri,
|
|
262
|
+
old_source_hash, new_source_hash, old_mirror_hash, new_mirror_hash,
|
|
263
|
+
old_active, new_active, heading_delta_json, link_delta_json,
|
|
264
|
+
typed_edge_delta_json, date_delta_json, structure_truncated,
|
|
265
|
+
observed_at_ms, byte_size
|
|
266
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
267
|
+
[
|
|
268
|
+
draft.documentId,
|
|
269
|
+
draft.collection,
|
|
270
|
+
draft.kind,
|
|
271
|
+
old?.relPath ?? null,
|
|
272
|
+
next?.relPath ?? null,
|
|
273
|
+
old?.docid ?? null,
|
|
274
|
+
next?.docid ?? null,
|
|
275
|
+
old?.uri ?? null,
|
|
276
|
+
next?.uri ?? null,
|
|
277
|
+
old?.sourceHash ?? null,
|
|
278
|
+
next?.sourceHash ?? null,
|
|
279
|
+
old?.mirrorHash ?? null,
|
|
280
|
+
next?.mirrorHash ?? null,
|
|
281
|
+
old === null ? null : old.active ? 1 : 0,
|
|
282
|
+
next === null ? null : next.active ? 1 : 0,
|
|
283
|
+
headingDeltaJson,
|
|
284
|
+
linkDeltaJson,
|
|
285
|
+
typedEdgeDeltaJson,
|
|
286
|
+
dateDeltaJson,
|
|
287
|
+
delta.truncated ? 1 : 0,
|
|
288
|
+
draft.observedAtMs,
|
|
289
|
+
storedByteSize,
|
|
290
|
+
]
|
|
291
|
+
);
|
|
292
|
+
db.run(
|
|
293
|
+
`UPDATE document_change_journal_state
|
|
294
|
+
SET last_sequence = ?,
|
|
295
|
+
retained_entries = retained_entries + 1,
|
|
296
|
+
retained_bytes = retained_bytes + ?
|
|
297
|
+
WHERE singleton_id = 1`,
|
|
298
|
+
[Number(inserted.lastInsertRowid), storedByteSize]
|
|
299
|
+
);
|
|
300
|
+
enforceInTransaction(
|
|
301
|
+
db,
|
|
302
|
+
DEFAULT_DOCUMENT_CHANGE_RETENTION,
|
|
303
|
+
draft.observedAtMs
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export const listDocumentChanges = (
|
|
308
|
+
db: Database,
|
|
309
|
+
options: DocumentChangeListOptions = {}
|
|
310
|
+
): StoreResult<DocumentChangePage> => {
|
|
311
|
+
try {
|
|
312
|
+
const limit = options.limit ?? 100;
|
|
313
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_SIZE) {
|
|
314
|
+
return err(
|
|
315
|
+
"INVALID_INPUT",
|
|
316
|
+
`Document change limit must be between 1 and ${MAX_PAGE_SIZE}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (
|
|
320
|
+
options.documentId !== undefined &&
|
|
321
|
+
(!Number.isSafeInteger(options.documentId) || options.documentId < 1)
|
|
322
|
+
) {
|
|
323
|
+
return err("INVALID_INPUT", "Document id must be a positive integer");
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
options.observedAfterMs !== undefined &&
|
|
327
|
+
(!Number.isSafeInteger(options.observedAfterMs) ||
|
|
328
|
+
options.observedAfterMs < 0)
|
|
329
|
+
) {
|
|
330
|
+
return err(
|
|
331
|
+
"INVALID_INPUT",
|
|
332
|
+
"Document change observedAfterMs must be a non-negative integer"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const state = db
|
|
337
|
+
.query<{ last_sequence: number; retention_floor: number }, []>(
|
|
338
|
+
`SELECT last_sequence, retention_floor
|
|
339
|
+
FROM document_change_journal_state
|
|
340
|
+
WHERE singleton_id = 1`
|
|
341
|
+
)
|
|
342
|
+
.get() ?? { last_sequence: 0, retention_floor: 0 };
|
|
343
|
+
const cursorSequence =
|
|
344
|
+
options.cursor === undefined
|
|
345
|
+
? state.retention_floor
|
|
346
|
+
: decodeDocumentChangeCursor(options.cursor);
|
|
347
|
+
const earliestCursor = encodeDocumentChangeCursor(state.retention_floor);
|
|
348
|
+
const latestCursor = encodeDocumentChangeCursor(state.last_sequence);
|
|
349
|
+
if (cursorSequence < state.retention_floor) {
|
|
350
|
+
return ok({
|
|
351
|
+
changes: [],
|
|
352
|
+
nextCursor: null,
|
|
353
|
+
earliestCursor,
|
|
354
|
+
latestCursor,
|
|
355
|
+
cursorExpired: true,
|
|
356
|
+
truncated: false,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (cursorSequence > state.last_sequence) {
|
|
360
|
+
return err(
|
|
361
|
+
"INVALID_INPUT",
|
|
362
|
+
"Document change cursor is ahead of the journal"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const conditions = ["sequence > ?"];
|
|
367
|
+
const params: (number | string)[] = [cursorSequence];
|
|
368
|
+
if (options.collection) {
|
|
369
|
+
conditions.push("collection = ?");
|
|
370
|
+
params.push(options.collection);
|
|
371
|
+
}
|
|
372
|
+
if (options.documentId !== undefined) {
|
|
373
|
+
conditions.push("document_id = ?");
|
|
374
|
+
params.push(options.documentId);
|
|
375
|
+
}
|
|
376
|
+
if (options.observedAfterMs !== undefined) {
|
|
377
|
+
conditions.push("observed_at_ms >= ?");
|
|
378
|
+
params.push(options.observedAfterMs);
|
|
379
|
+
}
|
|
380
|
+
const rows = db
|
|
381
|
+
.query<DbDocumentChangeRow, (number | string)[]>(
|
|
382
|
+
`SELECT *
|
|
383
|
+
FROM document_changes
|
|
384
|
+
WHERE ${conditions.join(" AND ")}
|
|
385
|
+
ORDER BY sequence ASC
|
|
386
|
+
LIMIT ?`
|
|
387
|
+
)
|
|
388
|
+
.all(...params, limit + 1);
|
|
389
|
+
const truncated = rows.length > limit;
|
|
390
|
+
const selected = rows.slice(0, limit).map(mapRow);
|
|
391
|
+
const lastReturned = selected.at(-1);
|
|
392
|
+
return ok({
|
|
393
|
+
changes: selected,
|
|
394
|
+
nextCursor:
|
|
395
|
+
truncated && lastReturned
|
|
396
|
+
? encodeDocumentChangeCursor(lastReturned.sequence)
|
|
397
|
+
: null,
|
|
398
|
+
earliestCursor,
|
|
399
|
+
latestCursor,
|
|
400
|
+
cursorExpired: false,
|
|
401
|
+
truncated,
|
|
402
|
+
});
|
|
403
|
+
} catch (cause) {
|
|
404
|
+
const invalidCursor =
|
|
405
|
+
cause instanceof TypeError &&
|
|
406
|
+
cause.message === "Invalid document change cursor";
|
|
407
|
+
return err(
|
|
408
|
+
invalidCursor ? "INVALID_INPUT" : "QUERY_FAILED",
|
|
409
|
+
cause instanceof Error
|
|
410
|
+
? cause.message
|
|
411
|
+
: "Failed to list document changes",
|
|
412
|
+
cause
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
export const enforceDocumentChangeRetention = (
|
|
418
|
+
db: Database,
|
|
419
|
+
policy: DocumentChangeRetentionPolicy,
|
|
420
|
+
nowMs: number
|
|
421
|
+
): StoreResult<DocumentChangeRetentionResult> => {
|
|
422
|
+
try {
|
|
423
|
+
const transaction = db.transaction(() =>
|
|
424
|
+
enforceInTransaction(db, policy, nowMs)
|
|
425
|
+
);
|
|
426
|
+
return ok(transaction());
|
|
427
|
+
} catch (cause) {
|
|
428
|
+
return err(
|
|
429
|
+
cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
|
|
430
|
+
cause instanceof Error
|
|
431
|
+
? cause.message
|
|
432
|
+
: "Failed to enforce document change retention",
|
|
433
|
+
cause
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
export const purgeDocumentChanges = (
|
|
439
|
+
db: Database
|
|
440
|
+
): StoreResult<DocumentChangePurgeResult> => {
|
|
441
|
+
try {
|
|
442
|
+
const transaction = db.transaction((): DocumentChangePurgeResult => {
|
|
443
|
+
const state = db
|
|
444
|
+
.query<{ last_sequence: number }, []>(
|
|
445
|
+
`SELECT last_sequence
|
|
446
|
+
FROM document_change_journal_state
|
|
447
|
+
WHERE singleton_id = 1`
|
|
448
|
+
)
|
|
449
|
+
.get() ?? { last_sequence: 0 };
|
|
450
|
+
const deleted = db.run("DELETE FROM document_changes").changes;
|
|
451
|
+
db.run(
|
|
452
|
+
`UPDATE document_change_journal_state
|
|
453
|
+
SET retention_floor = last_sequence,
|
|
454
|
+
retained_entries = 0,
|
|
455
|
+
retained_bytes = 0
|
|
456
|
+
WHERE singleton_id = 1`
|
|
457
|
+
);
|
|
458
|
+
return {
|
|
459
|
+
deleted,
|
|
460
|
+
earliestCursor: encodeDocumentChangeCursor(state.last_sequence),
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
return ok(transaction());
|
|
464
|
+
} catch (cause) {
|
|
465
|
+
return err(
|
|
466
|
+
"QUERY_FAILED",
|
|
467
|
+
cause instanceof Error
|
|
468
|
+
? cause.message
|
|
469
|
+
: "Failed to purge document changes",
|
|
470
|
+
cause
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
};
|