@smithers-orchestrator/db 0.20.3 → 0.20.4
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/package.json +5 -7
- package/src/adapter/SmithersDb.js +3 -2317
|
@@ -1,2317 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { getSqlMessageStorage } from "../sql-message-storage.js";
|
|
5
|
-
import { alertsAcknowledgedTotal, alertsFiredTotal, dbQueryDuration, dbTransactionDuration, dbTransactionRollbacks, } from "@smithers-orchestrator/observability/metrics";
|
|
6
|
-
import { assertOptionalStringMaxLength, assertPositiveFiniteNumber, } from "../input-bounds.js";
|
|
7
|
-
import { FRAME_KEYFRAME_INTERVAL, applyFrameDeltaJson, encodeFrameDelta, normalizeFrameEncoding, serializeFrameDelta, } from "../frame-codec.js";
|
|
8
|
-
import { getKeyColumns } from "../output.js";
|
|
9
|
-
import { withSqliteWriteRetryEffect } from "../write-retry.js";
|
|
10
|
-
import { camelToSnake } from "../utils/camelToSnake.js";
|
|
11
|
-
import { DB_ALERT_ID_MAX_LENGTH } from "./DB_ALERT_ID_MAX_LENGTH.js";
|
|
12
|
-
import { DB_ALERT_POLICY_NAME_MAX_LENGTH } from "./DB_ALERT_POLICY_NAME_MAX_LENGTH.js";
|
|
13
|
-
import { DB_ALERT_MESSAGE_MAX_LENGTH } from "./DB_ALERT_MESSAGE_MAX_LENGTH.js";
|
|
14
|
-
import { DB_ALERT_ALLOWED_SEVERITIES } from "./DB_ALERT_ALLOWED_SEVERITIES.js";
|
|
15
|
-
import { DB_ALERT_ALLOWED_STATUSES } from "./DB_ALERT_ALLOWED_STATUSES.js";
|
|
16
|
-
import { DB_RUN_ID_MAX_LENGTH } from "./DB_RUN_ID_MAX_LENGTH.js";
|
|
17
|
-
import { DB_RUN_WORKFLOW_NAME_MAX_LENGTH } from "./DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js";
|
|
18
|
-
import { DB_RUN_ALLOWED_STATUSES } from "./DB_RUN_ALLOWED_STATUSES.js";
|
|
19
|
-
import { alertsActive } from "@smithers-orchestrator/observability/metrics";
|
|
20
|
-
/** @typedef {import("./AlertRow.ts").AlertRow} AlertRow */
|
|
21
|
-
/** @typedef {import("./AlertStatus.ts").AlertStatus} AlertStatus */
|
|
22
|
-
/** @typedef {import("./AttemptRow.ts").AttemptRow} AttemptRow */
|
|
23
|
-
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
24
|
-
/** @typedef {import("./EventHistoryQuery.ts").EventHistoryQuery} EventHistoryQuery */
|
|
25
|
-
/** @typedef {import("./HumanRequestRow.ts").HumanRequestRow} HumanRequestRow */
|
|
26
|
-
/** @typedef {import("../output/OutputKey.ts").OutputKey} OutputKey */
|
|
27
|
-
/**
|
|
28
|
-
* @template A, E
|
|
29
|
-
* @typedef {Effect.Effect<A, E> & PromiseLike<A>} RunnableEffect
|
|
30
|
-
*/
|
|
31
|
-
/** @typedef {import("./SignalQuery.ts").SignalQuery} SignalQuery */
|
|
32
|
-
/** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
|
|
33
|
-
/**
|
|
34
|
-
* @typedef {{ runId: string; frameNo: number; createdAtMs: number; xmlJson: string; xmlHash: string; encoding: string; mountedTaskIdsJson: string | null; taskIndexJson: string | null; note: string | null; }} FrameRow
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* @typedef {{ runId: string; nodeId: string; iteration: number; baseRef: string; diffJson: string; computedAtMs: number; sizeBytes: number; }} NodeDiffCacheRow
|
|
38
|
-
*/
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {{ count: number }} CountRow
|
|
41
|
-
*/
|
|
42
|
-
/**
|
|
43
|
-
* @typedef {{ ralphId: string; runId: string; done?: boolean }} RalphRow
|
|
44
|
-
*/
|
|
45
|
-
/**
|
|
46
|
-
* @typedef {{ cacheKey: string; createdAtMs?: number; nodeId: string; outputTable: string }} CacheRowLike
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @typedef {{ runId: string; frameNo: number; createdAtMs: number; xmlJson: string; xmlHash: string; encoding: string; mountedTaskIdsJson: string | null; taskIndexJson: string | null; note: string | null; }} FrameRow
|
|
51
|
-
*/
|
|
52
|
-
/**
|
|
53
|
-
* @typedef {{ runId: string; nodeId: string; iteration: number; baseRef: string; diffJson: string; computedAtMs: number; sizeBytes: number; }} NodeDiffCacheRow
|
|
54
|
-
*/
|
|
55
|
-
/**
|
|
56
|
-
* @typedef {{ count: number }} CountRow
|
|
57
|
-
*/
|
|
58
|
-
/**
|
|
59
|
-
* @typedef {{ ralphId: string; runId: string; done?: boolean }} RalphRow
|
|
60
|
-
*/
|
|
61
|
-
/**
|
|
62
|
-
* @typedef {{ cacheKey: string; createdAtMs?: number; nodeId: string; outputTable: string }} CacheRowLike
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const FRAME_XML_CACHE_MAX = 512;
|
|
67
|
-
const RAW_QUERY_ALLOWED_PREFIX = /^(?:select|with|explain|values)\b/i;
|
|
68
|
-
const RAW_QUERY_FORBIDDEN_KEYWORDS = /\b(?:drop|delete|insert|update|alter|create|attach|detach|pragma)\b/i;
|
|
69
|
-
const ACTIVE_ALERT_STATUSES = new Set([
|
|
70
|
-
"firing",
|
|
71
|
-
"acknowledged",
|
|
72
|
-
"silenced",
|
|
73
|
-
]);
|
|
74
|
-
/**
|
|
75
|
-
* @param {string} queryString
|
|
76
|
-
* @returns {string}
|
|
77
|
-
*/
|
|
78
|
-
function stripSqlCommentsAndLiterals(queryString) {
|
|
79
|
-
let sanitized = "";
|
|
80
|
-
let index = 0;
|
|
81
|
-
while (index < queryString.length) {
|
|
82
|
-
const char = queryString[index];
|
|
83
|
-
const nextChar = queryString[index + 1];
|
|
84
|
-
if (char === "-" && nextChar === "-") {
|
|
85
|
-
sanitized += " ";
|
|
86
|
-
index += 2;
|
|
87
|
-
while (index < queryString.length && queryString[index] !== "\n") {
|
|
88
|
-
index += 1;
|
|
89
|
-
}
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (char === "/" && nextChar === "*") {
|
|
93
|
-
sanitized += " ";
|
|
94
|
-
index += 2;
|
|
95
|
-
while (index < queryString.length) {
|
|
96
|
-
if (queryString[index] === "*" && queryString[index + 1] === "/") {
|
|
97
|
-
index += 2;
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
index += 1;
|
|
101
|
-
}
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (char === "'" || char === "\"" || char === "`") {
|
|
105
|
-
const quote = char;
|
|
106
|
-
sanitized += " ";
|
|
107
|
-
index += 1;
|
|
108
|
-
while (index < queryString.length) {
|
|
109
|
-
if (queryString[index] === quote) {
|
|
110
|
-
if (queryString[index + 1] === quote) {
|
|
111
|
-
index += 2;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
index += 1;
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
index += 1;
|
|
118
|
-
}
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (char === "[") {
|
|
122
|
-
sanitized += " ";
|
|
123
|
-
index += 1;
|
|
124
|
-
while (index < queryString.length) {
|
|
125
|
-
if (queryString[index] === "]") {
|
|
126
|
-
index += 1;
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
index += 1;
|
|
130
|
-
}
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
sanitized += char;
|
|
134
|
-
index += 1;
|
|
135
|
-
}
|
|
136
|
-
return sanitized;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* @param {string} queryString
|
|
140
|
-
* @returns {string}
|
|
141
|
-
*/
|
|
142
|
-
function validateReadOnlyRawQuery(queryString) {
|
|
143
|
-
const trimmedQuery = queryString.trim();
|
|
144
|
-
if (!trimmedQuery) {
|
|
145
|
-
throw toSmithersError(new Error("Raw query must not be empty"), undefined, {
|
|
146
|
-
code: "INVALID_INPUT",
|
|
147
|
-
details: { operation: "raw query validation" },
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
const sanitizedQuery = stripSqlCommentsAndLiterals(trimmedQuery).trim();
|
|
151
|
-
if (!sanitizedQuery) {
|
|
152
|
-
throw toSmithersError(new Error("Raw query must not be empty"), undefined, {
|
|
153
|
-
code: "INVALID_INPUT",
|
|
154
|
-
details: { operation: "raw query validation" },
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
const singleStatementQuery = sanitizedQuery.replace(/;+\s*$/, "").trim();
|
|
158
|
-
if (singleStatementQuery.includes(";")) {
|
|
159
|
-
throw toSmithersError(new Error("Raw query must contain a single read-only SQL statement"), undefined, {
|
|
160
|
-
code: "INVALID_INPUT",
|
|
161
|
-
details: { operation: "raw query validation" },
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
const forbiddenKeyword = singleStatementQuery.match(RAW_QUERY_FORBIDDEN_KEYWORDS)?.[0];
|
|
165
|
-
if (forbiddenKeyword) {
|
|
166
|
-
throw toSmithersError(new Error(`Raw query cannot use ${forbiddenKeyword.toUpperCase()} statements`), undefined, {
|
|
167
|
-
code: "INVALID_INPUT",
|
|
168
|
-
details: {
|
|
169
|
-
operation: "raw query validation",
|
|
170
|
-
keyword: forbiddenKeyword.toUpperCase(),
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
if (!RAW_QUERY_ALLOWED_PREFIX.test(singleStatementQuery)) {
|
|
175
|
-
throw toSmithersError(new Error("Raw query only supports read-only SELECT, WITH, EXPLAIN, or VALUES statements"), undefined, {
|
|
176
|
-
code: "INVALID_INPUT",
|
|
177
|
-
details: { operation: "raw query validation" },
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
return trimmedQuery;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* @param {unknown} status
|
|
184
|
-
*/
|
|
185
|
-
function validateRunStatus(status) {
|
|
186
|
-
if (typeof status !== "string" ||
|
|
187
|
-
!DB_RUN_ALLOWED_STATUSES.includes(status)) {
|
|
188
|
-
throw toSmithersError(new Error("Invalid run status"), `Run status must be one of: ${DB_RUN_ALLOWED_STATUSES.join(", ")}`, {
|
|
189
|
-
code: "INVALID_INPUT",
|
|
190
|
-
details: { status },
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* @param {unknown} severity
|
|
196
|
-
*/
|
|
197
|
-
function validateAlertSeverity(severity) {
|
|
198
|
-
if (typeof severity !== "string" ||
|
|
199
|
-
!DB_ALERT_ALLOWED_SEVERITIES.includes(severity)) {
|
|
200
|
-
throw toSmithersError(new Error("Invalid alert severity"), `Alert severity must be one of: ${DB_ALERT_ALLOWED_SEVERITIES.join(", ")}`, {
|
|
201
|
-
code: "INVALID_INPUT",
|
|
202
|
-
details: { severity },
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* @param {unknown} status
|
|
208
|
-
*/
|
|
209
|
-
function validateAlertStatus(status) {
|
|
210
|
-
if (typeof status !== "string" ||
|
|
211
|
-
!DB_ALERT_ALLOWED_STATUSES.includes(status)) {
|
|
212
|
-
throw toSmithersError(new Error("Invalid alert status"), `Alert status must be one of: ${DB_ALERT_ALLOWED_STATUSES.join(", ")}`, {
|
|
213
|
-
code: "INVALID_INPUT",
|
|
214
|
-
details: { status },
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* @param {Record<string, unknown>} row
|
|
220
|
-
* @param {string} field
|
|
221
|
-
*/
|
|
222
|
-
function validateOptionalPositiveTimestamp(row, field) {
|
|
223
|
-
const value = row[field];
|
|
224
|
-
if (value === undefined || value === null)
|
|
225
|
-
return;
|
|
226
|
-
assertPositiveFiniteNumber(field, Number(value));
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* @param {unknown} row
|
|
230
|
-
* @returns {void}
|
|
231
|
-
*/
|
|
232
|
-
function validateRunRow(row) {
|
|
233
|
-
if (!row || typeof row !== "object") {
|
|
234
|
-
throw toSmithersError(new Error("Invalid run row"), "Run row must be an object", {
|
|
235
|
-
code: "INVALID_INPUT",
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
const r = /** @type {Record<string, unknown>} */ (row);
|
|
239
|
-
assertOptionalStringMaxLength("runId", r.runId, DB_RUN_ID_MAX_LENGTH);
|
|
240
|
-
assertOptionalStringMaxLength("parentRunId", r.parentRunId, DB_RUN_ID_MAX_LENGTH);
|
|
241
|
-
assertOptionalStringMaxLength("workflowName", r.workflowName, DB_RUN_WORKFLOW_NAME_MAX_LENGTH);
|
|
242
|
-
validateRunStatus(r.status);
|
|
243
|
-
validateOptionalPositiveTimestamp(r, "createdAtMs");
|
|
244
|
-
validateOptionalPositiveTimestamp(r, "startedAtMs");
|
|
245
|
-
validateOptionalPositiveTimestamp(r, "finishedAtMs");
|
|
246
|
-
validateOptionalPositiveTimestamp(r, "heartbeatAtMs");
|
|
247
|
-
validateOptionalPositiveTimestamp(r, "cancelRequestedAtMs");
|
|
248
|
-
validateOptionalPositiveTimestamp(r, "hijackRequestedAtMs");
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* @param {unknown} patch
|
|
252
|
-
* @returns {void}
|
|
253
|
-
*/
|
|
254
|
-
function validateRunPatch(patch) {
|
|
255
|
-
if (!patch || typeof patch !== "object")
|
|
256
|
-
return;
|
|
257
|
-
const p = /** @type {Record<string, unknown>} */ (patch);
|
|
258
|
-
if ("workflowName" in p) {
|
|
259
|
-
assertOptionalStringMaxLength("workflowName", p.workflowName, DB_RUN_WORKFLOW_NAME_MAX_LENGTH);
|
|
260
|
-
}
|
|
261
|
-
if ("status" in p) {
|
|
262
|
-
validateRunStatus(p.status);
|
|
263
|
-
}
|
|
264
|
-
validateOptionalPositiveTimestamp(p, "startedAtMs");
|
|
265
|
-
validateOptionalPositiveTimestamp(p, "finishedAtMs");
|
|
266
|
-
validateOptionalPositiveTimestamp(p, "heartbeatAtMs");
|
|
267
|
-
validateOptionalPositiveTimestamp(p, "cancelRequestedAtMs");
|
|
268
|
-
validateOptionalPositiveTimestamp(p, "hijackRequestedAtMs");
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* @param {AlertRow} row
|
|
272
|
-
*/
|
|
273
|
-
function validateAlertRow(row) {
|
|
274
|
-
if (!row || typeof row !== "object") {
|
|
275
|
-
throw toSmithersError(new Error("Invalid alert row"), "Alert row must be an object", { code: "INVALID_INPUT" });
|
|
276
|
-
}
|
|
277
|
-
assertOptionalStringMaxLength("alertId", row.alertId, DB_ALERT_ID_MAX_LENGTH);
|
|
278
|
-
assertOptionalStringMaxLength("runId", row.runId, DB_RUN_ID_MAX_LENGTH);
|
|
279
|
-
assertOptionalStringMaxLength("policyName", row.policyName, DB_ALERT_POLICY_NAME_MAX_LENGTH);
|
|
280
|
-
assertOptionalStringMaxLength("message", row.message, DB_ALERT_MESSAGE_MAX_LENGTH);
|
|
281
|
-
if (typeof row.alertId !== "string" || row.alertId.length === 0) {
|
|
282
|
-
throw toSmithersError(new Error("Invalid alert ID"), "Alert ID must be a non-empty string", { code: "INVALID_INPUT", details: { alertId: row.alertId } });
|
|
283
|
-
}
|
|
284
|
-
if (row.runId !== null && row.runId !== undefined && typeof row.runId !== "string") {
|
|
285
|
-
throw toSmithersError(new Error("Invalid alert run ID"), "Alert run ID must be a string or null", { code: "INVALID_INPUT", details: { runId: row.runId } });
|
|
286
|
-
}
|
|
287
|
-
if (typeof row.policyName !== "string" || row.policyName.length === 0) {
|
|
288
|
-
throw toSmithersError(new Error("Invalid alert policy name"), "Alert policy name must be a non-empty string", { code: "INVALID_INPUT", details: { policyName: row.policyName } });
|
|
289
|
-
}
|
|
290
|
-
if (typeof row.message !== "string" || row.message.length === 0) {
|
|
291
|
-
throw toSmithersError(new Error("Invalid alert message"), "Alert message must be a non-empty string", { code: "INVALID_INPUT", details: { message: row.message } });
|
|
292
|
-
}
|
|
293
|
-
if (row.detailsJson !== null &&
|
|
294
|
-
row.detailsJson !== undefined &&
|
|
295
|
-
typeof row.detailsJson !== "string") {
|
|
296
|
-
throw toSmithersError(new Error("Invalid alert details JSON"), "Alert details JSON must be a string or null", { code: "INVALID_INPUT", details: { detailsJson: row.detailsJson } });
|
|
297
|
-
}
|
|
298
|
-
validateAlertSeverity(row.severity);
|
|
299
|
-
validateAlertStatus(row.status);
|
|
300
|
-
validateOptionalPositiveTimestamp(row, "firedAtMs");
|
|
301
|
-
validateOptionalPositiveTimestamp(row, "resolvedAtMs");
|
|
302
|
-
validateOptionalPositiveTimestamp(row, "acknowledgedAtMs");
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* @param {string | null | undefined} status
|
|
306
|
-
* @returns {status is AlertStatus}
|
|
307
|
-
*/
|
|
308
|
-
function isAlertActiveStatus(status) {
|
|
309
|
-
return status !== undefined && status !== null && ACTIVE_ALERT_STATUSES.has(status);
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Returns the row unchanged. Heartbeat-based classification now lives in
|
|
313
|
-
* `deriveRunState`, which correctly returns "stale" / "orphaned" rather than
|
|
314
|
-
* misusing "continued" (which means the run forked into a new run, and is
|
|
315
|
-
* treated as a success by `deriveRunState`).
|
|
316
|
-
*
|
|
317
|
-
* @template T
|
|
318
|
-
* @param {T} row
|
|
319
|
-
* @returns {T}
|
|
320
|
-
*/
|
|
321
|
-
function classifyRunRowStatus(row) {
|
|
322
|
-
return row;
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* @template A, E
|
|
326
|
-
* @param {Effect.Effect<A, E>} effect
|
|
327
|
-
* @returns {RunnableEffect<A, E>}
|
|
328
|
-
*/
|
|
329
|
-
function runnableEffect(effect) {
|
|
330
|
-
const runnable = effect;
|
|
331
|
-
if (typeof runnable.then !== "function") {
|
|
332
|
-
Object.defineProperty(runnable, "then", {
|
|
333
|
-
configurable: true,
|
|
334
|
-
value: (onfulfilled, onrejected) => Effect.runPromise(effect).then(onfulfilled, onrejected),
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
return runnable;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* @typedef {{ depth: number; ownerThread: string | null; tail: Promise<unknown> }} SqliteTransactionState
|
|
341
|
-
*/
|
|
342
|
-
/** @type {WeakMap<object, SqliteTransactionState>} */
|
|
343
|
-
// Cross-adapter coordination: one sqlite connection cannot run overlapping BEGIN IMMEDIATE statements.
|
|
344
|
-
const sqliteTransactionStateByClient = (() => {
|
|
345
|
-
const key = Symbol.for("smithers.sqliteTransactionStateByClient");
|
|
346
|
-
const registry = /** @type {Record<PropertyKey, unknown>} */ (globalThis);
|
|
347
|
-
const existing = registry[key];
|
|
348
|
-
if (existing instanceof WeakMap) {
|
|
349
|
-
return /** @type {WeakMap<object, SqliteTransactionState>} */ (existing);
|
|
350
|
-
}
|
|
351
|
-
const stateByClient = new WeakMap();
|
|
352
|
-
Object.defineProperty(globalThis, key, {
|
|
353
|
-
configurable: false,
|
|
354
|
-
enumerable: false,
|
|
355
|
-
value: stateByClient,
|
|
356
|
-
});
|
|
357
|
-
return stateByClient;
|
|
358
|
-
})();
|
|
359
|
-
/**
|
|
360
|
-
* @param {unknown} client
|
|
361
|
-
* @returns {object}
|
|
362
|
-
*/
|
|
363
|
-
function assertSqliteClientKey(client) {
|
|
364
|
-
if ((typeof client !== "object" && typeof client !== "function") ||
|
|
365
|
-
client === null) {
|
|
366
|
-
throw new Error("SmithersDb requires an object sqlite client for transaction coordination.");
|
|
367
|
-
}
|
|
368
|
-
return /** @type {object} */ (client);
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* @param {unknown} db
|
|
372
|
-
* @returns {object}
|
|
373
|
-
*/
|
|
374
|
-
function resolveSqliteClientKey(db) {
|
|
375
|
-
const source = /** @type {{ session?: { client?: unknown }; $client?: unknown }} */ (db);
|
|
376
|
-
return assertSqliteClientKey(source?.session?.client ?? source?.$client ?? db);
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* @param {unknown} client
|
|
380
|
-
* @returns {SqliteTransactionState}
|
|
381
|
-
*/
|
|
382
|
-
function getSqliteTransactionState(client) {
|
|
383
|
-
const key = assertSqliteClientKey(client);
|
|
384
|
-
let state = sqliteTransactionStateByClient.get(key);
|
|
385
|
-
if (!state) {
|
|
386
|
-
state = {
|
|
387
|
-
depth: 0,
|
|
388
|
-
ownerThread: null,
|
|
389
|
-
tail: Promise.resolve(),
|
|
390
|
-
};
|
|
391
|
-
sqliteTransactionStateByClient.set(key, state);
|
|
392
|
-
}
|
|
393
|
-
return state;
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* @param {unknown} db
|
|
397
|
-
* @returns {{ run: (sql: string) => unknown; query: (sql: string) => { run: (...args: unknown[]) => unknown; get: (...args: unknown[]) => Record<string, unknown> | null | undefined; all: () => Array<Record<string, unknown>> }; exec: (sql: string) => unknown; $client?: unknown }}
|
|
398
|
-
*/
|
|
399
|
-
function resolveSqliteTransactionClient(db) {
|
|
400
|
-
const candidate = /** @type {{ run?: unknown; query?: unknown; exec?: unknown; $client?: unknown }} */ (resolveSqliteClientKey(db));
|
|
401
|
-
if (typeof candidate.run !== "function") {
|
|
402
|
-
throw new Error("SmithersDb.withTransaction requires Bun SQLite client transaction primitives.");
|
|
403
|
-
}
|
|
404
|
-
return /** @type {{ run: (sql: string) => unknown; query: (sql: string) => { run: (...args: unknown[]) => unknown; get: (...args: unknown[]) => Record<string, unknown> | null | undefined; all: () => Array<Record<string, unknown>> }; exec: (sql: string) => unknown; $client?: unknown }} */ (candidate);
|
|
405
|
-
}
|
|
406
|
-
export class SmithersDb {
|
|
407
|
-
/** @type {BunSQLiteDatabase<Record<string, unknown>>} */
|
|
408
|
-
db;
|
|
409
|
-
/** @type {ReturnType<typeof getSqlMessageStorage>} */
|
|
410
|
-
internalStorage;
|
|
411
|
-
/** @type {Map<string, string>} */
|
|
412
|
-
reconstructedFrameXmlCache = new Map();
|
|
413
|
-
transactionDepth = 0;
|
|
414
|
-
/** @type {string | null} */
|
|
415
|
-
transactionOwnerThread = null;
|
|
416
|
-
/** @type {Promise<unknown>} */
|
|
417
|
-
transactionTail = Promise.resolve();
|
|
418
|
-
/**
|
|
419
|
-
* @param {BunSQLiteDatabase<Record<string, unknown>>} db
|
|
420
|
-
*/
|
|
421
|
-
constructor(db) {
|
|
422
|
-
this.db = db;
|
|
423
|
-
this.internalStorage = getSqlMessageStorage(db);
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* @param {string} runId
|
|
427
|
-
* @param {number} frameNo
|
|
428
|
-
* @returns {string}
|
|
429
|
-
*/
|
|
430
|
-
frameCacheKey(runId, frameNo) {
|
|
431
|
-
return `${runId}:${frameNo}`;
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* @param {string} runId
|
|
435
|
-
* @param {number} frameNo
|
|
436
|
-
* @returns {string | undefined}
|
|
437
|
-
*/
|
|
438
|
-
getCachedFrameXml(runId, frameNo) {
|
|
439
|
-
const key = this.frameCacheKey(runId, frameNo);
|
|
440
|
-
const value = this.reconstructedFrameXmlCache.get(key);
|
|
441
|
-
if (value === undefined)
|
|
442
|
-
return undefined;
|
|
443
|
-
// Keep recently-used entries hot.
|
|
444
|
-
this.reconstructedFrameXmlCache.delete(key);
|
|
445
|
-
this.reconstructedFrameXmlCache.set(key, value);
|
|
446
|
-
return value;
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* @param {string} runId
|
|
450
|
-
* @param {number} frameNo
|
|
451
|
-
* @param {string} xmlJson
|
|
452
|
-
*/
|
|
453
|
-
rememberFrameXml(runId, frameNo, xmlJson) {
|
|
454
|
-
const key = this.frameCacheKey(runId, frameNo);
|
|
455
|
-
if (this.reconstructedFrameXmlCache.has(key)) {
|
|
456
|
-
this.reconstructedFrameXmlCache.delete(key);
|
|
457
|
-
}
|
|
458
|
-
else if (this.reconstructedFrameXmlCache.size >= FRAME_XML_CACHE_MAX) {
|
|
459
|
-
const oldest = this.reconstructedFrameXmlCache.keys().next().value;
|
|
460
|
-
if (oldest !== undefined) {
|
|
461
|
-
this.reconstructedFrameXmlCache.delete(oldest);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
this.reconstructedFrameXmlCache.set(key, xmlJson);
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* @param {string} runId
|
|
468
|
-
*/
|
|
469
|
-
clearFrameCacheForRun(runId) {
|
|
470
|
-
for (const key of this.reconstructedFrameXmlCache.keys()) {
|
|
471
|
-
if (key.startsWith(`${runId}:`)) {
|
|
472
|
-
this.reconstructedFrameXmlCache.delete(key);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* @param {string} queryString
|
|
478
|
-
* @returns {RunnableEffect<unknown[], SmithersError>}
|
|
479
|
-
*/
|
|
480
|
-
rawQuery(queryString) {
|
|
481
|
-
const self = this;
|
|
482
|
-
return runnableEffect(Effect.gen(function* () {
|
|
483
|
-
const validatedQuery = yield* Effect.try({
|
|
484
|
-
try: () => validateReadOnlyRawQuery(queryString),
|
|
485
|
-
catch: (cause) => toSmithersError(cause, "validate raw query", {
|
|
486
|
-
code: "INVALID_INPUT",
|
|
487
|
-
details: { operation: "raw query validation" },
|
|
488
|
-
}),
|
|
489
|
-
});
|
|
490
|
-
return yield* self.read(`raw query ${validatedQuery.slice(0, 20)}`, () => {
|
|
491
|
-
const client = self.db.session.client;
|
|
492
|
-
const stmt = client.query(validatedQuery);
|
|
493
|
-
return Promise.resolve(stmt.all());
|
|
494
|
-
});
|
|
495
|
-
}));
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* @param {string} currentFiberThread
|
|
499
|
-
* @returns {boolean}
|
|
500
|
-
*/
|
|
501
|
-
ownsActiveTransaction(currentFiberThread) {
|
|
502
|
-
const state = getSqliteTransactionState(resolveSqliteClientKey(this.db));
|
|
503
|
-
this.transactionDepth = state.depth;
|
|
504
|
-
this.transactionOwnerThread = state.ownerThread;
|
|
505
|
-
this.transactionTail = state.tail;
|
|
506
|
-
return (state.depth > 0 &&
|
|
507
|
-
state.ownerThread === currentFiberThread);
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* @template A
|
|
511
|
-
* @param {string} label
|
|
512
|
-
* @param {() => PromiseLike<A>} operation
|
|
513
|
-
* @returns {RunnableEffect<A, SmithersError>}
|
|
514
|
-
*/
|
|
515
|
-
read(label, operation) {
|
|
516
|
-
const self = this;
|
|
517
|
-
return runnableEffect(Effect.gen(function* () {
|
|
518
|
-
const start = performance.now();
|
|
519
|
-
const readOperation = Effect.tryPromise({
|
|
520
|
-
try: () => operation(),
|
|
521
|
-
catch: (cause) => toSmithersError(cause, label, {
|
|
522
|
-
code: "DB_QUERY_FAILED",
|
|
523
|
-
details: { operation: label },
|
|
524
|
-
}),
|
|
525
|
-
});
|
|
526
|
-
const currentFiberId = yield* Effect.fiberId;
|
|
527
|
-
const currentFiberThread = FiberId.threadName(currentFiberId);
|
|
528
|
-
let result;
|
|
529
|
-
if (self.ownsActiveTransaction(currentFiberThread)) {
|
|
530
|
-
result = yield* readOperation;
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
const releaseTurn = yield* self.acquireTransactionTurn();
|
|
534
|
-
result = yield* readOperation.pipe(Effect.ensuring(Effect.sync(() => {
|
|
535
|
-
releaseTurn();
|
|
536
|
-
})));
|
|
537
|
-
}
|
|
538
|
-
yield* Metric.update(dbQueryDuration, performance.now() - start);
|
|
539
|
-
return result;
|
|
540
|
-
}).pipe(Effect.annotateLogs({ dbOperation: label }), Effect.withLogSpan(`db:${label}`)));
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* @template A
|
|
544
|
-
* @param {string} label
|
|
545
|
-
* @param {() => PromiseLike<A>} operation
|
|
546
|
-
* @returns {RunnableEffect<A, SmithersError>}
|
|
547
|
-
*/
|
|
548
|
-
write(label, operation) {
|
|
549
|
-
const self = this;
|
|
550
|
-
return runnableEffect(Effect.gen(function* () {
|
|
551
|
-
const start = performance.now();
|
|
552
|
-
const writeOperation = Effect.tryPromise({
|
|
553
|
-
try: () => operation(),
|
|
554
|
-
catch: (cause) => toSmithersError(cause, label, {
|
|
555
|
-
code: "DB_WRITE_FAILED",
|
|
556
|
-
details: { operation: label },
|
|
557
|
-
}),
|
|
558
|
-
});
|
|
559
|
-
const currentFiberId = yield* Effect.fiberId;
|
|
560
|
-
const currentFiberThread = FiberId.threadName(currentFiberId);
|
|
561
|
-
let result;
|
|
562
|
-
if (self.ownsActiveTransaction(currentFiberThread)) {
|
|
563
|
-
result = yield* writeOperation;
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
566
|
-
const releaseTurn = yield* self.acquireTransactionTurn();
|
|
567
|
-
result = yield* withSqliteWriteRetryEffect(() => writeOperation, { label }).pipe(Effect.ensuring(Effect.sync(() => {
|
|
568
|
-
releaseTurn();
|
|
569
|
-
})));
|
|
570
|
-
}
|
|
571
|
-
yield* Metric.update(dbQueryDuration, performance.now() - start);
|
|
572
|
-
return result;
|
|
573
|
-
}).pipe(Effect.annotateLogs({ dbOperation: label }), Effect.withLogSpan(`db:${label}`)));
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* @returns {Effect.Effect<{ run: (sql: string) => unknown; query: (sql: string) => { run: (...args: unknown[]) => unknown; get: (...args: unknown[]) => Record<string, unknown> | null | undefined; all: () => Array<Record<string, unknown>> }; exec: (sql: string) => unknown; $client?: unknown }, SmithersError, never>}
|
|
577
|
-
*/
|
|
578
|
-
getSqliteTransactionClient() {
|
|
579
|
-
return Effect.try({
|
|
580
|
-
try: () => {
|
|
581
|
-
return resolveSqliteTransactionClient(this.db);
|
|
582
|
-
},
|
|
583
|
-
catch: (cause) => toSmithersError(cause, "resolve sqlite transaction client", {
|
|
584
|
-
code: "DB_WRITE_FAILED",
|
|
585
|
-
details: { operation: "resolve sqlite transaction client" },
|
|
586
|
-
}),
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
/**
|
|
590
|
-
* @returns {Effect.Effect<() => void, SmithersError, never>}
|
|
591
|
-
*/
|
|
592
|
-
acquireTransactionTurn() {
|
|
593
|
-
return Effect.tryPromise({
|
|
594
|
-
try: async () => {
|
|
595
|
-
const state = getSqliteTransactionState(resolveSqliteClientKey(this.db));
|
|
596
|
-
let release;
|
|
597
|
-
const gate = new Promise((resolve) => {
|
|
598
|
-
release = resolve;
|
|
599
|
-
});
|
|
600
|
-
const previous = state.tail.catch(() => undefined);
|
|
601
|
-
state.tail = previous.then(() => gate);
|
|
602
|
-
this.transactionTail = state.tail;
|
|
603
|
-
await previous;
|
|
604
|
-
return release;
|
|
605
|
-
},
|
|
606
|
-
catch: (cause) => toSmithersError(cause, "acquire sqlite transaction turn", {
|
|
607
|
-
code: "DB_WRITE_FAILED",
|
|
608
|
-
details: { operation: "acquire sqlite transaction turn" },
|
|
609
|
-
}),
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* @template A
|
|
614
|
-
* @param {string} writeGroup
|
|
615
|
-
* @param {Effect.Effect<A, SmithersError>} operation
|
|
616
|
-
* @returns {RunnableEffect<A, SmithersError>}
|
|
617
|
-
*/
|
|
618
|
-
withTransactionEffect(writeGroup, operation) {
|
|
619
|
-
const self = this;
|
|
620
|
-
const label = `sqlite transaction ${writeGroup}`;
|
|
621
|
-
return runnableEffect(withSqliteWriteRetryEffect(() => Effect.gen(function* () {
|
|
622
|
-
const currentFiberId = yield* Effect.fiberId;
|
|
623
|
-
const currentFiberThread = FiberId.threadName(currentFiberId);
|
|
624
|
-
if (self.ownsActiveTransaction(currentFiberThread)) {
|
|
625
|
-
return yield* Effect.fail(toSmithersError(new Error(`Nested sqlite transactions are not supported (writeGroup: ${writeGroup}).`), label, {
|
|
626
|
-
code: "DB_WRITE_FAILED",
|
|
627
|
-
details: { writeGroup, nestedTransaction: true },
|
|
628
|
-
}));
|
|
629
|
-
}
|
|
630
|
-
const releaseTurn = yield* self.acquireTransactionTurn();
|
|
631
|
-
const transactionState = getSqliteTransactionState(resolveSqliteClientKey(self.db));
|
|
632
|
-
const start = performance.now();
|
|
633
|
-
return yield* Effect.gen(function* () {
|
|
634
|
-
const client = yield* self.getSqliteTransactionClient();
|
|
635
|
-
/**
|
|
636
|
-
* @param {"operation" | "commit"} phase
|
|
637
|
-
* @param {unknown} error
|
|
638
|
-
*/
|
|
639
|
-
const rollback = (phase, error) => Effect.gen(function* () {
|
|
640
|
-
yield* Metric.increment(dbTransactionRollbacks);
|
|
641
|
-
yield* Effect.logWarning("transaction rollback").pipe(Effect.annotateLogs({
|
|
642
|
-
writeGroup,
|
|
643
|
-
phase,
|
|
644
|
-
error: String(error),
|
|
645
|
-
}));
|
|
646
|
-
yield* Effect.sync(() => {
|
|
647
|
-
try {
|
|
648
|
-
client.run("ROLLBACK");
|
|
649
|
-
}
|
|
650
|
-
catch {
|
|
651
|
-
// ignore rollback failures
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
});
|
|
655
|
-
yield* Effect.try({
|
|
656
|
-
try: () => {
|
|
657
|
-
client.run("BEGIN IMMEDIATE");
|
|
658
|
-
transactionState.depth += 1;
|
|
659
|
-
transactionState.ownerThread = currentFiberThread;
|
|
660
|
-
self.transactionDepth = transactionState.depth;
|
|
661
|
-
self.transactionOwnerThread = transactionState.ownerThread;
|
|
662
|
-
self.transactionTail = transactionState.tail;
|
|
663
|
-
},
|
|
664
|
-
catch: (cause) => toSmithersError(cause, "begin sqlite transaction", {
|
|
665
|
-
code: "DB_WRITE_FAILED",
|
|
666
|
-
details: { writeGroup, phase: "begin" },
|
|
667
|
-
}),
|
|
668
|
-
});
|
|
669
|
-
const operationExit = yield* Effect.exit(operation);
|
|
670
|
-
if (Exit.isFailure(operationExit)) {
|
|
671
|
-
yield* rollback("operation", operationExit.cause);
|
|
672
|
-
return yield* Effect.failCause(operationExit.cause);
|
|
673
|
-
}
|
|
674
|
-
const commitExit = yield* Effect.exit(Effect.try({
|
|
675
|
-
try: () => {
|
|
676
|
-
client.run("COMMIT");
|
|
677
|
-
},
|
|
678
|
-
catch: (cause) => toSmithersError(cause, "commit sqlite transaction", {
|
|
679
|
-
code: "DB_WRITE_FAILED",
|
|
680
|
-
details: { writeGroup, phase: "commit" },
|
|
681
|
-
}),
|
|
682
|
-
}));
|
|
683
|
-
if (Exit.isFailure(commitExit)) {
|
|
684
|
-
yield* rollback("commit", commitExit.cause);
|
|
685
|
-
return yield* Effect.failCause(commitExit.cause);
|
|
686
|
-
}
|
|
687
|
-
return operationExit.value;
|
|
688
|
-
}).pipe(Effect.ensuring(Effect.gen(function* () {
|
|
689
|
-
transactionState.depth = Math.max(0, transactionState.depth - 1);
|
|
690
|
-
if (transactionState.depth === 0) {
|
|
691
|
-
transactionState.ownerThread = null;
|
|
692
|
-
}
|
|
693
|
-
self.transactionDepth = transactionState.depth;
|
|
694
|
-
self.transactionOwnerThread = transactionState.ownerThread;
|
|
695
|
-
self.transactionTail = transactionState.tail;
|
|
696
|
-
yield* Metric.update(dbTransactionDuration, performance.now() - start);
|
|
697
|
-
}))).pipe(Effect.ensuring(Effect.sync(() => {
|
|
698
|
-
releaseTurn();
|
|
699
|
-
})));
|
|
700
|
-
}), { label }).pipe(Effect.annotateLogs({ writeGroup }), Effect.withLogSpan("db:transaction")));
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* @template A
|
|
704
|
-
* @param {string} writeGroup
|
|
705
|
-
* @param {Effect.Effect<A, SmithersError>} operation
|
|
706
|
-
* @returns {Promise<A>}
|
|
707
|
-
*/
|
|
708
|
-
withTransaction(writeGroup, operation) {
|
|
709
|
-
return Effect.runPromise(this.withTransactionEffect(writeGroup, operation));
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* @param {Record<string, unknown>} row
|
|
713
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
714
|
-
*/
|
|
715
|
-
insertRun(row) {
|
|
716
|
-
validateRunRow(row);
|
|
717
|
-
return this.write("insert run", () => this.internalStorage.insertIgnore("_smithers_runs", row));
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* @param {string} runId
|
|
721
|
-
* @param {Record<string, unknown>} patch
|
|
722
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
723
|
-
*/
|
|
724
|
-
updateRun(runId, patch) {
|
|
725
|
-
validateRunPatch(patch);
|
|
726
|
-
return this.write(`update run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", patch, "run_id = ?", [runId]));
|
|
727
|
-
}
|
|
728
|
-
/**
|
|
729
|
-
* @param {string} runId
|
|
730
|
-
* @param {Record<string, unknown>} patch
|
|
731
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
732
|
-
*/
|
|
733
|
-
updateRunEffect(runId, patch) {
|
|
734
|
-
return this.updateRun(runId, patch);
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* @param {string} runId
|
|
738
|
-
* @param {string} runtimeOwnerId
|
|
739
|
-
* @param {number} heartbeatAtMs
|
|
740
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
741
|
-
*/
|
|
742
|
-
heartbeatRun(runId, runtimeOwnerId, heartbeatAtMs) {
|
|
743
|
-
return this.write(`heartbeat run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", { heartbeatAtMs }, "run_id = ? AND runtime_owner_id = ?", [runId, runtimeOwnerId]));
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* @param {string} runId
|
|
747
|
-
* @param {number} cancelRequestedAtMs
|
|
748
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
749
|
-
*/
|
|
750
|
-
requestRunCancel(runId, cancelRequestedAtMs) {
|
|
751
|
-
return this.write(`cancel run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", { cancelRequestedAtMs }, "run_id = ?", [runId]));
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* @param {string} runId
|
|
755
|
-
* @param {number} hijackRequestedAtMs
|
|
756
|
-
* @param {string | null} [hijackTarget]
|
|
757
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
758
|
-
*/
|
|
759
|
-
requestRunHijack(runId, hijackRequestedAtMs, hijackTarget) {
|
|
760
|
-
return this.write(`hijack run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", {
|
|
761
|
-
hijackRequestedAtMs,
|
|
762
|
-
hijackTarget: hijackTarget ?? null,
|
|
763
|
-
}, "run_id = ?", [runId]));
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* @param {string} runId
|
|
767
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
768
|
-
*/
|
|
769
|
-
clearRunHijack(runId) {
|
|
770
|
-
return this.write(`clear hijack run ${runId}`, () => this.internalStorage.updateWhere("_smithers_runs", {
|
|
771
|
-
hijackRequestedAtMs: null,
|
|
772
|
-
hijackTarget: null,
|
|
773
|
-
}, "run_id = ?", [runId]));
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* @param {string} runId
|
|
777
|
-
* @returns {RunnableEffect<RunRow | undefined, SmithersError>}
|
|
778
|
-
*/
|
|
779
|
-
getRun(runId) {
|
|
780
|
-
return this.read(`get run ${runId}`, async () => {
|
|
781
|
-
const row = await this.internalStorage.queryOne(`SELECT *
|
|
782
|
-
FROM _smithers_runs
|
|
783
|
-
WHERE run_id = ?
|
|
784
|
-
LIMIT 1`, [runId]);
|
|
785
|
-
return row ? classifyRunRowStatus(row) : undefined;
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* @param {string} runId
|
|
790
|
-
* @returns {RunnableEffect<RunAncestryRow[], SmithersError>}
|
|
791
|
-
*/
|
|
792
|
-
listRunAncestry(runId, limit = 1000) {
|
|
793
|
-
return this.read(`list run ancestry ${runId}`, () => this.internalStorage.queryAll(`WITH RECURSIVE ancestry(run_id, parent_run_id, depth) AS (
|
|
794
|
-
SELECT run_id, parent_run_id, 0
|
|
795
|
-
FROM _smithers_runs
|
|
796
|
-
WHERE run_id = ?
|
|
797
|
-
UNION ALL
|
|
798
|
-
SELECT child.run_id, child.parent_run_id, ancestry.depth + 1
|
|
799
|
-
FROM _smithers_runs child
|
|
800
|
-
JOIN ancestry ON child.run_id = ancestry.parent_run_id
|
|
801
|
-
WHERE ancestry.parent_run_id IS NOT NULL
|
|
802
|
-
)
|
|
803
|
-
SELECT
|
|
804
|
-
run_id,
|
|
805
|
-
parent_run_id,
|
|
806
|
-
depth
|
|
807
|
-
FROM ancestry
|
|
808
|
-
ORDER BY depth ASC
|
|
809
|
-
LIMIT ?`, [runId, limit]));
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* @param {string} parentRunId
|
|
813
|
-
* @returns {RunnableEffect<RunRow | undefined, SmithersError>}
|
|
814
|
-
*/
|
|
815
|
-
getLatestChildRun(parentRunId) {
|
|
816
|
-
return this.read(`get latest child run ${parentRunId}`, () => this.internalStorage.queryOne(`SELECT *
|
|
817
|
-
FROM _smithers_runs
|
|
818
|
-
WHERE parent_run_id = ?
|
|
819
|
-
ORDER BY created_at_ms DESC
|
|
820
|
-
LIMIT 1`, [parentRunId]));
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* @param {string} [status]
|
|
824
|
-
* @returns {RunnableEffect<RunRow[], SmithersError>}
|
|
825
|
-
*/
|
|
826
|
-
listRuns(limit = 50, status) {
|
|
827
|
-
return this.read(`list runs ${status ?? "all"}`, async () => {
|
|
828
|
-
const clauses = [];
|
|
829
|
-
const params = [];
|
|
830
|
-
if (status === "running") {
|
|
831
|
-
clauses.push("(status = ? OR status = ?)");
|
|
832
|
-
params.push("running", "continued");
|
|
833
|
-
}
|
|
834
|
-
else if (status) {
|
|
835
|
-
clauses.push("status = ?");
|
|
836
|
-
params.push(status);
|
|
837
|
-
}
|
|
838
|
-
const whereSql = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
839
|
-
const rows = await this.internalStorage.queryAll(`SELECT *
|
|
840
|
-
FROM _smithers_runs
|
|
841
|
-
${whereSql}
|
|
842
|
-
ORDER BY created_at_ms DESC
|
|
843
|
-
LIMIT ?`, [...params, limit]);
|
|
844
|
-
return rows.map((row) => classifyRunRowStatus(row));
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
/**
|
|
848
|
-
* @param {number} staleBeforeMs
|
|
849
|
-
* @returns {RunnableEffect<StaleRunRecord[], SmithersError>}
|
|
850
|
-
*/
|
|
851
|
-
listStaleRunningRuns(staleBeforeMs, limit = 1000) {
|
|
852
|
-
return this.read(`list stale running runs before ${staleBeforeMs}`, () => this.internalStorage.queryAll(`SELECT
|
|
853
|
-
run_id,
|
|
854
|
-
workflow_path,
|
|
855
|
-
heartbeat_at_ms,
|
|
856
|
-
runtime_owner_id,
|
|
857
|
-
status
|
|
858
|
-
FROM _smithers_runs
|
|
859
|
-
WHERE status = 'running'
|
|
860
|
-
AND (heartbeat_at_ms IS NULL OR heartbeat_at_ms < ?)
|
|
861
|
-
ORDER BY COALESCE(heartbeat_at_ms, 0) ASC
|
|
862
|
-
LIMIT ?`, [staleBeforeMs, limit]));
|
|
863
|
-
}
|
|
864
|
-
/**
|
|
865
|
-
* @param {{ runId: string; expectedStatus?: string; expectedRuntimeOwnerId: string | null; expectedHeartbeatAtMs: number | null; staleBeforeMs: number; claimOwnerId: string; claimHeartbeatAtMs: number; requireStale?: boolean; }} params
|
|
866
|
-
* @returns {RunnableEffect<boolean, SmithersError>}
|
|
867
|
-
*/
|
|
868
|
-
claimRunForResume(params) {
|
|
869
|
-
return this.write(`claim stale run ${params.runId}`, () => {
|
|
870
|
-
const client = this.db.session.client;
|
|
871
|
-
const expectedStatus = params.expectedStatus ?? "running";
|
|
872
|
-
const requireStale = params.requireStale ?? expectedStatus === "running";
|
|
873
|
-
client
|
|
874
|
-
.query(`UPDATE _smithers_runs
|
|
875
|
-
SET runtime_owner_id = ?, heartbeat_at_ms = ?
|
|
876
|
-
WHERE run_id = ?
|
|
877
|
-
AND status = ?
|
|
878
|
-
AND COALESCE(runtime_owner_id, '') = COALESCE(?, '')
|
|
879
|
-
AND COALESCE(heartbeat_at_ms, -1) = COALESCE(?, -1)
|
|
880
|
-
AND (? = 0 OR heartbeat_at_ms IS NULL OR heartbeat_at_ms < ?)`)
|
|
881
|
-
.run(params.claimOwnerId, params.claimHeartbeatAtMs, params.runId, expectedStatus, params.expectedRuntimeOwnerId, params.expectedHeartbeatAtMs, requireStale ? 1 : 0, params.staleBeforeMs);
|
|
882
|
-
return this.internalStorage
|
|
883
|
-
.queryOne("SELECT changes() AS count")
|
|
884
|
-
.then((row) => Number(row?.count ?? 0) > 0);
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
/**
|
|
888
|
-
* @param {{ runId: string; claimOwnerId: string; restoreRuntimeOwnerId: string | null; restoreHeartbeatAtMs: number | null; }} params
|
|
889
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
890
|
-
*/
|
|
891
|
-
releaseRunResumeClaim(params) {
|
|
892
|
-
return this.write(`release stale run claim ${params.runId}`, () => {
|
|
893
|
-
return this.internalStorage.execute(`UPDATE _smithers_runs
|
|
894
|
-
SET runtime_owner_id = ?, heartbeat_at_ms = ?
|
|
895
|
-
WHERE run_id = ? AND runtime_owner_id = ?`, [
|
|
896
|
-
params.restoreRuntimeOwnerId,
|
|
897
|
-
params.restoreHeartbeatAtMs,
|
|
898
|
-
params.runId,
|
|
899
|
-
params.claimOwnerId,
|
|
900
|
-
]);
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* @param {{ runId: string; expectedRuntimeOwnerId: string; expectedHeartbeatAtMs: number | null; patch: Record<string, unknown>; }} params
|
|
905
|
-
* @returns {RunnableEffect<boolean, SmithersError>}
|
|
906
|
-
*/
|
|
907
|
-
updateClaimedRun(params) {
|
|
908
|
-
validateRunPatch(params.patch);
|
|
909
|
-
return this.write(`update claimed run ${params.runId}`, () => {
|
|
910
|
-
const client = this.db.session.client;
|
|
911
|
-
const patchEntries = Object.entries(params.patch);
|
|
912
|
-
if (patchEntries.length === 0) {
|
|
913
|
-
return Promise.resolve(true);
|
|
914
|
-
}
|
|
915
|
-
const assignments = patchEntries.map(([key]) => `${camelToSnake(key)} = ?`);
|
|
916
|
-
client
|
|
917
|
-
.query(`UPDATE _smithers_runs
|
|
918
|
-
SET ${assignments.join(", ")}
|
|
919
|
-
WHERE run_id = ?
|
|
920
|
-
AND runtime_owner_id = ?
|
|
921
|
-
AND COALESCE(heartbeat_at_ms, -1) = COALESCE(?, -1)`)
|
|
922
|
-
.run(...patchEntries.map(([, value]) => value), params.runId, params.expectedRuntimeOwnerId, params.expectedHeartbeatAtMs);
|
|
923
|
-
return this.internalStorage
|
|
924
|
-
.queryOne("SELECT changes() AS count")
|
|
925
|
-
.then((row) => Number(row?.count ?? 0) > 0);
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* @param {Record<string, unknown>} row
|
|
930
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
931
|
-
*/
|
|
932
|
-
insertNode(row) {
|
|
933
|
-
return this.insertNodeEffect(row);
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* @param {Record<string, unknown>} row
|
|
937
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
938
|
-
*/
|
|
939
|
-
insertNodeEffect(row) {
|
|
940
|
-
return this.write(`insert node ${row.nodeId}`, () => this.internalStorage.upsert("_smithers_nodes", row, ["runId", "nodeId", "iteration"]));
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* @param {string} runId
|
|
944
|
-
* @param {string} nodeId
|
|
945
|
-
* @param {number} iteration
|
|
946
|
-
* @returns {RunnableEffect<NodeRow | undefined, SmithersError>}
|
|
947
|
-
*/
|
|
948
|
-
getNode(runId, nodeId, iteration) {
|
|
949
|
-
return this.read(`get node ${nodeId}`, () => this.internalStorage.queryOne(`SELECT *
|
|
950
|
-
FROM _smithers_nodes
|
|
951
|
-
WHERE run_id = ? AND node_id = ? AND iteration = ?
|
|
952
|
-
LIMIT 1`, [runId, nodeId, iteration]));
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* @param {string} runId
|
|
956
|
-
* @param {string} nodeId
|
|
957
|
-
* @returns {RunnableEffect<NodeRow[], SmithersError>}
|
|
958
|
-
*/
|
|
959
|
-
listNodeIterations(runId, nodeId) {
|
|
960
|
-
return this.read(`list node iterations ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
961
|
-
FROM _smithers_nodes
|
|
962
|
-
WHERE run_id = ? AND node_id = ?
|
|
963
|
-
ORDER BY iteration DESC`, [runId, nodeId]));
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* @param {string} runId
|
|
967
|
-
* @returns {RunnableEffect<NodeRow[], SmithersError>}
|
|
968
|
-
*/
|
|
969
|
-
listNodes(runId) {
|
|
970
|
-
return this.read(`list nodes ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
971
|
-
FROM _smithers_nodes
|
|
972
|
-
WHERE run_id = ?`, [runId]));
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* @param {Table} table
|
|
976
|
-
* @param {OutputKey} key
|
|
977
|
-
* @param {Record<string, unknown>} payload
|
|
978
|
-
* @returns {RunnableEffect<unknown, SmithersError>}
|
|
979
|
-
*/
|
|
980
|
-
upsertOutputRow(table, key, payload) {
|
|
981
|
-
const cols = getKeyColumns(table);
|
|
982
|
-
const values = { ...payload };
|
|
983
|
-
values.runId = key.runId;
|
|
984
|
-
values.nodeId = key.nodeId;
|
|
985
|
-
if (cols.iteration) {
|
|
986
|
-
values.iteration = key.iteration ?? 0;
|
|
987
|
-
}
|
|
988
|
-
const target = cols.iteration
|
|
989
|
-
? [cols.runId, cols.nodeId, cols.iteration]
|
|
990
|
-
: [cols.runId, cols.nodeId];
|
|
991
|
-
const tableName = table?.["_"]?.name ?? "output";
|
|
992
|
-
return this.write(`upsert output ${tableName}`, () => this.db
|
|
993
|
-
.insert(table)
|
|
994
|
-
.values(values)
|
|
995
|
-
.onConflictDoUpdate({
|
|
996
|
-
target: target,
|
|
997
|
-
set: values,
|
|
998
|
-
}));
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* @param {Table} table
|
|
1002
|
-
* @param {OutputKey} key
|
|
1003
|
-
* @param {Record<string, unknown>} payload
|
|
1004
|
-
* @returns {RunnableEffect<unknown, SmithersError>}
|
|
1005
|
-
*/
|
|
1006
|
-
upsertOutputRowEffect(table, key, payload) {
|
|
1007
|
-
return this.upsertOutputRow(table, key, payload);
|
|
1008
|
-
}
|
|
1009
|
-
/**
|
|
1010
|
-
* @param {string} tableName
|
|
1011
|
-
* @param {OutputKey} key
|
|
1012
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1013
|
-
*/
|
|
1014
|
-
deleteOutputRow(tableName, key) {
|
|
1015
|
-
return this.write(`delete output ${tableName}`, () => {
|
|
1016
|
-
const client = this.db.session.client;
|
|
1017
|
-
let resolvedTableName = tableName;
|
|
1018
|
-
let escapedTableName = resolvedTableName.replaceAll(`"`, `""`);
|
|
1019
|
-
let tableInfo = client
|
|
1020
|
-
.query(`PRAGMA table_info("${escapedTableName}")`)
|
|
1021
|
-
.all();
|
|
1022
|
-
if (tableInfo.length === 0) {
|
|
1023
|
-
const schemaCandidates = [
|
|
1024
|
-
this.db?._?.fullSchema,
|
|
1025
|
-
this.db?._?.schema,
|
|
1026
|
-
this.db?.schema,
|
|
1027
|
-
];
|
|
1028
|
-
for (const candidate of schemaCandidates) {
|
|
1029
|
-
if (!candidate || typeof candidate !== "object")
|
|
1030
|
-
continue;
|
|
1031
|
-
const table = candidate[tableName];
|
|
1032
|
-
if (!table)
|
|
1033
|
-
continue;
|
|
1034
|
-
try {
|
|
1035
|
-
resolvedTableName = getTableName(table);
|
|
1036
|
-
escapedTableName = resolvedTableName.replaceAll(`"`, `""`);
|
|
1037
|
-
tableInfo = client
|
|
1038
|
-
.query(`PRAGMA table_info("${escapedTableName}")`)
|
|
1039
|
-
.all();
|
|
1040
|
-
if (tableInfo.length > 0) {
|
|
1041
|
-
break;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
catch { }
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
const columnNames = new Set(tableInfo
|
|
1048
|
-
.map((column) => column.name)
|
|
1049
|
-
.filter((name) => typeof name === "string"));
|
|
1050
|
-
const runIdColumn = columnNames.has("run_id")
|
|
1051
|
-
? "run_id"
|
|
1052
|
-
: columnNames.has("runId")
|
|
1053
|
-
? "runId"
|
|
1054
|
-
: null;
|
|
1055
|
-
const nodeIdColumn = columnNames.has("node_id")
|
|
1056
|
-
? "node_id"
|
|
1057
|
-
: columnNames.has("nodeId")
|
|
1058
|
-
? "nodeId"
|
|
1059
|
-
: null;
|
|
1060
|
-
const iterationColumn = columnNames.has("iteration")
|
|
1061
|
-
? "iteration"
|
|
1062
|
-
: null;
|
|
1063
|
-
if (!runIdColumn || !nodeIdColumn) {
|
|
1064
|
-
throw new Error(`Output table ${tableName} is missing runId/nodeId columns`);
|
|
1065
|
-
}
|
|
1066
|
-
if (iterationColumn) {
|
|
1067
|
-
client
|
|
1068
|
-
.query(`DELETE FROM "${escapedTableName}"
|
|
1069
|
-
WHERE "${runIdColumn}" = ? AND "${nodeIdColumn}" = ? AND "${iterationColumn}" = ?`)
|
|
1070
|
-
.run(key.runId, key.nodeId, key.iteration ?? 0);
|
|
1071
|
-
}
|
|
1072
|
-
else {
|
|
1073
|
-
client
|
|
1074
|
-
.query(`DELETE FROM "${escapedTableName}"
|
|
1075
|
-
WHERE "${runIdColumn}" = ? AND "${nodeIdColumn}" = ?`)
|
|
1076
|
-
.run(key.runId, key.nodeId);
|
|
1077
|
-
}
|
|
1078
|
-
return Promise.resolve(undefined);
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* @param {string} tableName
|
|
1083
|
-
* @param {OutputKey} key
|
|
1084
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1085
|
-
*/
|
|
1086
|
-
deleteOutputRowEffect(tableName, key) {
|
|
1087
|
-
return this.deleteOutputRow(tableName, key);
|
|
1088
|
-
}
|
|
1089
|
-
/**
|
|
1090
|
-
* @param {string} tableName
|
|
1091
|
-
* @param {string} runId
|
|
1092
|
-
* @param {string} nodeId
|
|
1093
|
-
* @returns {RunnableEffect<Record<string, unknown> | null, SmithersError>}
|
|
1094
|
-
*/
|
|
1095
|
-
getRawNodeOutput(tableName, runId, nodeId) {
|
|
1096
|
-
return runnableEffect(this.read(`get raw node output ${tableName}`, () => {
|
|
1097
|
-
const query = sql.raw(`SELECT * FROM "${tableName}" WHERE run_id = '${runId}' AND node_id = '${nodeId}' ORDER BY iteration DESC LIMIT 1`);
|
|
1098
|
-
const res = this.db.get(query);
|
|
1099
|
-
return Promise.resolve(res ?? null);
|
|
1100
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null))));
|
|
1101
|
-
}
|
|
1102
|
-
/**
|
|
1103
|
-
* @param {string} tableName
|
|
1104
|
-
* @param {string} runId
|
|
1105
|
-
* @param {string} nodeId
|
|
1106
|
-
* @param {number} iteration
|
|
1107
|
-
* @returns {RunnableEffect<Record<string, unknown> | null, SmithersError>}
|
|
1108
|
-
*/
|
|
1109
|
-
getRawNodeOutputForIteration(tableName, runId, nodeId, iteration) {
|
|
1110
|
-
return runnableEffect(this.read(`get raw node output ${tableName} iteration ${iteration}`, () => {
|
|
1111
|
-
const escaped = tableName.replaceAll(`"`, `""`);
|
|
1112
|
-
const client = this.db.session.client;
|
|
1113
|
-
const stmt = client.query(`SELECT * FROM "${escaped}" WHERE run_id = ? AND node_id = ? AND iteration = ? LIMIT 1`);
|
|
1114
|
-
const row = stmt.get(runId, nodeId, iteration);
|
|
1115
|
-
return Promise.resolve(row ?? null);
|
|
1116
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null))));
|
|
1117
|
-
}
|
|
1118
|
-
/**
|
|
1119
|
-
* @param {Record<string, unknown>} row
|
|
1120
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1121
|
-
*/
|
|
1122
|
-
insertAttempt(row) {
|
|
1123
|
-
return this.write(`insert attempt ${row.nodeId}#${row.attempt}`, () => this.internalStorage.upsert("_smithers_attempts", row, ["runId", "nodeId", "iteration", "attempt"]));
|
|
1124
|
-
}
|
|
1125
|
-
/**
|
|
1126
|
-
* @param {Record<string, unknown>} row
|
|
1127
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1128
|
-
*/
|
|
1129
|
-
insertAttemptEffect(row) {
|
|
1130
|
-
return this.insertAttempt(row);
|
|
1131
|
-
}
|
|
1132
|
-
/**
|
|
1133
|
-
* @param {string} runId
|
|
1134
|
-
* @param {string} nodeId
|
|
1135
|
-
* @param {number} iteration
|
|
1136
|
-
* @param {number} attempt
|
|
1137
|
-
* @param {Record<string, unknown>} patch
|
|
1138
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1139
|
-
*/
|
|
1140
|
-
updateAttempt(runId, nodeId, iteration, attempt, patch) {
|
|
1141
|
-
return this.write(`update attempt ${nodeId}#${attempt}`, () => this.internalStorage.updateWhere("_smithers_attempts", patch, "run_id = ? AND node_id = ? AND iteration = ? AND attempt = ?", [runId, nodeId, iteration, attempt]));
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* @param {string} runId
|
|
1145
|
-
* @param {string} nodeId
|
|
1146
|
-
* @param {number} iteration
|
|
1147
|
-
* @param {number} attempt
|
|
1148
|
-
* @param {Record<string, unknown>} patch
|
|
1149
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1150
|
-
*/
|
|
1151
|
-
updateAttemptEffect(runId, nodeId, iteration, attempt, patch) {
|
|
1152
|
-
return this.updateAttempt(runId, nodeId, iteration, attempt, patch);
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* @param {string} runId
|
|
1156
|
-
* @param {string} nodeId
|
|
1157
|
-
* @param {number} iteration
|
|
1158
|
-
* @param {number} attempt
|
|
1159
|
-
* @param {number} heartbeatAtMs
|
|
1160
|
-
* @param {string | null} heartbeatDataJson
|
|
1161
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1162
|
-
*/
|
|
1163
|
-
heartbeatAttempt(runId, nodeId, iteration, attempt, heartbeatAtMs, heartbeatDataJson) {
|
|
1164
|
-
return this.write(`heartbeat attempt ${nodeId}#${attempt}`, () => this.internalStorage.updateWhere("_smithers_attempts", {
|
|
1165
|
-
heartbeatAtMs,
|
|
1166
|
-
heartbeatDataJson,
|
|
1167
|
-
}, "run_id = ? AND node_id = ? AND iteration = ? AND attempt = ? AND state = ?", [runId, nodeId, iteration, attempt, "in-progress"]));
|
|
1168
|
-
}
|
|
1169
|
-
/**
|
|
1170
|
-
* @param {string} runId
|
|
1171
|
-
* @param {string} nodeId
|
|
1172
|
-
* @param {number} iteration
|
|
1173
|
-
* @returns {RunnableEffect<AttemptRow[], SmithersError>}
|
|
1174
|
-
*/
|
|
1175
|
-
listAttempts(runId, nodeId, iteration) {
|
|
1176
|
-
return this.read(`list attempts ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1177
|
-
FROM _smithers_attempts
|
|
1178
|
-
WHERE run_id = ? AND node_id = ? AND iteration = ?
|
|
1179
|
-
ORDER BY attempt DESC`, [runId, nodeId, iteration], { booleanColumns: ["cached"] }));
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* @param {string} runId
|
|
1183
|
-
* @returns {RunnableEffect<AttemptRow[], SmithersError>}
|
|
1184
|
-
*/
|
|
1185
|
-
listAttemptsForRun(runId) {
|
|
1186
|
-
return this.read(`list attempts for run ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1187
|
-
FROM _smithers_attempts
|
|
1188
|
-
WHERE run_id = ?
|
|
1189
|
-
ORDER BY started_at_ms ASC, node_id ASC, iteration ASC, attempt ASC`, [runId], { booleanColumns: ["cached"] }));
|
|
1190
|
-
}
|
|
1191
|
-
/**
|
|
1192
|
-
* @param {string} runId
|
|
1193
|
-
* @param {string} nodeId
|
|
1194
|
-
* @param {number} iteration
|
|
1195
|
-
* @param {number} attempt
|
|
1196
|
-
* @returns {RunnableEffect<AttemptRow | undefined, SmithersError>}
|
|
1197
|
-
*/
|
|
1198
|
-
getAttempt(runId, nodeId, iteration, attempt) {
|
|
1199
|
-
return this.read(`get attempt ${nodeId}#${attempt}`, () => this.internalStorage.queryOne(`SELECT *
|
|
1200
|
-
FROM _smithers_attempts
|
|
1201
|
-
WHERE run_id = ? AND node_id = ? AND iteration = ? AND attempt = ?
|
|
1202
|
-
LIMIT 1`, [runId, nodeId, iteration, attempt], { booleanColumns: ["cached"] }));
|
|
1203
|
-
}
|
|
1204
|
-
/**
|
|
1205
|
-
* @param {string} runId
|
|
1206
|
-
* @returns {RunnableEffect<AttemptRow[], SmithersError>}
|
|
1207
|
-
*/
|
|
1208
|
-
listInProgressAttempts(runId) {
|
|
1209
|
-
return this.read(`list in-progress attempts ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1210
|
-
FROM _smithers_attempts
|
|
1211
|
-
WHERE run_id = ? AND state = ?`, [runId, "in-progress"], { booleanColumns: ["cached"] }));
|
|
1212
|
-
}
|
|
1213
|
-
/**
|
|
1214
|
-
* @returns {RunnableEffect<AttemptRow[], SmithersError>}
|
|
1215
|
-
*/
|
|
1216
|
-
listAllInProgressAttempts() {
|
|
1217
|
-
return this.read("list all in-progress attempts", () => this.internalStorage.queryAll(`SELECT *
|
|
1218
|
-
FROM _smithers_attempts
|
|
1219
|
-
WHERE state = ?`, ["in-progress"], { booleanColumns: ["cached"] }));
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* @param {string} runId
|
|
1223
|
-
* @param {number} frameNo
|
|
1224
|
-
* @param {number} [limit]
|
|
1225
|
-
* @returns {RunnableEffect<FrameRow[], SmithersError>}
|
|
1226
|
-
*/
|
|
1227
|
-
listFrameChainDesc(runId, frameNo, limit) {
|
|
1228
|
-
return this.read(`list frame chain ${runId}:${frameNo}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1229
|
-
FROM _smithers_frames
|
|
1230
|
-
WHERE run_id = ? AND frame_no <= ?
|
|
1231
|
-
ORDER BY frame_no DESC${typeof limit === "number" ? " LIMIT ?" : ""}`, typeof limit === "number" ? [runId, frameNo, limit] : [runId, frameNo]));
|
|
1232
|
-
}
|
|
1233
|
-
/**
|
|
1234
|
-
* @param {string} runId
|
|
1235
|
-
* @param {number} frameNo
|
|
1236
|
-
* @param {Map<number, string>} [localCache]
|
|
1237
|
-
* @returns {Effect.Effect<string | undefined, SmithersError>}
|
|
1238
|
-
*/
|
|
1239
|
-
reconstructFrameXml(runId, frameNo, localCache = new Map()) {
|
|
1240
|
-
const self = this;
|
|
1241
|
-
return Effect.gen(function* () {
|
|
1242
|
-
const localHit = localCache.get(frameNo);
|
|
1243
|
-
if (localHit !== undefined)
|
|
1244
|
-
return localHit;
|
|
1245
|
-
const cacheHit = self.getCachedFrameXml(runId, frameNo);
|
|
1246
|
-
if (cacheHit !== undefined) {
|
|
1247
|
-
localCache.set(frameNo, cacheHit);
|
|
1248
|
-
return cacheHit;
|
|
1249
|
-
}
|
|
1250
|
-
let rows = (yield* self.listFrameChainDesc(runId, frameNo, FRAME_KEYFRAME_INTERVAL + 2));
|
|
1251
|
-
if (rows.length === 0)
|
|
1252
|
-
return undefined;
|
|
1253
|
-
let anchorIndex = rows.findIndex((row) => normalizeFrameEncoding(row.encoding) !== "delta");
|
|
1254
|
-
if (anchorIndex < 0) {
|
|
1255
|
-
rows = (yield* self.listFrameChainDesc(runId, frameNo));
|
|
1256
|
-
anchorIndex = rows.findIndex((row) => normalizeFrameEncoding(row.encoding) !== "delta");
|
|
1257
|
-
}
|
|
1258
|
-
if (anchorIndex < 0) {
|
|
1259
|
-
return rows.find((row) => row.frameNo === frameNo)?.xmlJson;
|
|
1260
|
-
}
|
|
1261
|
-
const chain = rows.slice(0, anchorIndex + 1).reverse();
|
|
1262
|
-
let currentXml = "";
|
|
1263
|
-
for (const frameRow of chain) {
|
|
1264
|
-
const rowEncoding = normalizeFrameEncoding(frameRow.encoding);
|
|
1265
|
-
if (rowEncoding === "delta") {
|
|
1266
|
-
if (!currentXml) {
|
|
1267
|
-
currentXml = String(frameRow.xmlJson ?? "null");
|
|
1268
|
-
}
|
|
1269
|
-
else {
|
|
1270
|
-
currentXml = yield* Effect.try({
|
|
1271
|
-
try: () => applyFrameDeltaJson(currentXml, String(frameRow.xmlJson ?? "")),
|
|
1272
|
-
catch: (cause) => toSmithersError(cause, `apply frame delta ${runId}:${frameRow.frameNo}`, {
|
|
1273
|
-
code: "DB_QUERY_FAILED",
|
|
1274
|
-
details: { runId, frameNo: frameRow.frameNo },
|
|
1275
|
-
}),
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
else {
|
|
1280
|
-
currentXml = String(frameRow.xmlJson ?? "null");
|
|
1281
|
-
}
|
|
1282
|
-
localCache.set(frameRow.frameNo, currentXml);
|
|
1283
|
-
self.rememberFrameXml(runId, frameRow.frameNo, currentXml);
|
|
1284
|
-
}
|
|
1285
|
-
return localCache.get(frameNo);
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* @param {FrameRow} row
|
|
1290
|
-
* @param {Map<number, string>} [localCache]
|
|
1291
|
-
* @returns {Effect.Effect<FrameRow, SmithersError>}
|
|
1292
|
-
*/
|
|
1293
|
-
inflateFrameRow(row, localCache = new Map()) {
|
|
1294
|
-
const self = this;
|
|
1295
|
-
return Effect.gen(function* () {
|
|
1296
|
-
const encoding = normalizeFrameEncoding(row?.encoding);
|
|
1297
|
-
if (encoding !== "delta") {
|
|
1298
|
-
const xmlJson = String(row?.xmlJson ?? "null");
|
|
1299
|
-
localCache.set(row.frameNo, xmlJson);
|
|
1300
|
-
self.rememberFrameXml(row.runId, row.frameNo, xmlJson);
|
|
1301
|
-
return { ...row, encoding, xmlJson };
|
|
1302
|
-
}
|
|
1303
|
-
const xmlJson = yield* self.reconstructFrameXml(row.runId, row.frameNo, localCache);
|
|
1304
|
-
return {
|
|
1305
|
-
...row,
|
|
1306
|
-
encoding,
|
|
1307
|
-
xmlJson: xmlJson ?? String(row?.xmlJson ?? "null"),
|
|
1308
|
-
};
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
/**
|
|
1312
|
-
* @param {Record<string, unknown>} row
|
|
1313
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1314
|
-
*/
|
|
1315
|
-
insertFrame(row) {
|
|
1316
|
-
const self = this;
|
|
1317
|
-
return runnableEffect(Effect.gen(function* () {
|
|
1318
|
-
const runId = String(row.runId);
|
|
1319
|
-
const frameNo = Number(row.frameNo);
|
|
1320
|
-
const fullXmlJson = String(row.xmlJson ?? "null");
|
|
1321
|
-
let encoding = "keyframe";
|
|
1322
|
-
let persistedXmlJson = fullXmlJson;
|
|
1323
|
-
if (frameNo > 0 && frameNo % FRAME_KEYFRAME_INTERVAL !== 0) {
|
|
1324
|
-
const previousXmlJson = yield* self.reconstructFrameXml(runId, frameNo - 1);
|
|
1325
|
-
if (typeof previousXmlJson === "string") {
|
|
1326
|
-
const delta = yield* Effect.try({
|
|
1327
|
-
try: () => encodeFrameDelta(previousXmlJson, fullXmlJson),
|
|
1328
|
-
catch: (cause) => toSmithersError(cause, `encode frame delta ${runId}:${frameNo}`, {
|
|
1329
|
-
code: "DB_WRITE_FAILED",
|
|
1330
|
-
details: { runId, frameNo },
|
|
1331
|
-
}),
|
|
1332
|
-
});
|
|
1333
|
-
const deltaJson = serializeFrameDelta(delta);
|
|
1334
|
-
if (deltaJson.length < fullXmlJson.length) {
|
|
1335
|
-
encoding = "delta";
|
|
1336
|
-
persistedXmlJson = deltaJson;
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
const persistedRow = {
|
|
1341
|
-
...row,
|
|
1342
|
-
xmlJson: persistedXmlJson,
|
|
1343
|
-
encoding,
|
|
1344
|
-
};
|
|
1345
|
-
yield* self.write(`insert frame ${frameNo}`, () => self.internalStorage.upsert("_smithers_frames", persistedRow, ["runId", "frameNo"]));
|
|
1346
|
-
self.clearFrameCacheForRun(runId);
|
|
1347
|
-
self.rememberFrameXml(runId, frameNo, fullXmlJson);
|
|
1348
|
-
}));
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* @param {Record<string, unknown>} row
|
|
1352
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1353
|
-
*/
|
|
1354
|
-
insertFrameEffect(row) {
|
|
1355
|
-
return this.insertFrame(row);
|
|
1356
|
-
}
|
|
1357
|
-
/**
|
|
1358
|
-
* @param {string} runId
|
|
1359
|
-
* @returns {RunnableEffect<FrameRow | undefined, SmithersError>}
|
|
1360
|
-
*/
|
|
1361
|
-
getLastFrame(runId) {
|
|
1362
|
-
const self = this;
|
|
1363
|
-
return runnableEffect(Effect.gen(function* () {
|
|
1364
|
-
const row = yield* self.read(`get last frame ${runId}`, () => self.internalStorage.queryOne(`SELECT *
|
|
1365
|
-
FROM _smithers_frames
|
|
1366
|
-
WHERE run_id = ?
|
|
1367
|
-
ORDER BY frame_no DESC
|
|
1368
|
-
LIMIT 1`, [runId]));
|
|
1369
|
-
if (!row)
|
|
1370
|
-
return undefined;
|
|
1371
|
-
return yield* self.inflateFrameRow(row);
|
|
1372
|
-
}));
|
|
1373
|
-
}
|
|
1374
|
-
/**
|
|
1375
|
-
* @param {Record<string, unknown>} row
|
|
1376
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1377
|
-
*/
|
|
1378
|
-
insertOrUpdateApproval(row) {
|
|
1379
|
-
return this.write(`upsert approval ${row.nodeId}`, () => this.internalStorage.upsert("_smithers_approvals", row, ["runId", "nodeId", "iteration"]));
|
|
1380
|
-
}
|
|
1381
|
-
/**
|
|
1382
|
-
* @param {string} runId
|
|
1383
|
-
* @param {string} nodeId
|
|
1384
|
-
* @param {number} iteration
|
|
1385
|
-
* @returns {RunnableEffect<ApprovalRow | undefined, SmithersError>}
|
|
1386
|
-
*/
|
|
1387
|
-
getApproval(runId, nodeId, iteration) {
|
|
1388
|
-
return this.read(`get approval ${nodeId}`, () => this.internalStorage.queryOne(`SELECT *
|
|
1389
|
-
FROM _smithers_approvals
|
|
1390
|
-
WHERE run_id = ? AND node_id = ? AND iteration = ?
|
|
1391
|
-
LIMIT 1`, [runId, nodeId, iteration], { booleanColumns: ["autoApproved"] }));
|
|
1392
|
-
}
|
|
1393
|
-
/**
|
|
1394
|
-
* @param {HumanRequestRow} row
|
|
1395
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1396
|
-
*/
|
|
1397
|
-
insertHumanRequest(row) {
|
|
1398
|
-
return this.write(`insert human request ${row.requestId}`, () => this.internalStorage.insertIgnore("_smithers_human_requests", row));
|
|
1399
|
-
}
|
|
1400
|
-
/**
|
|
1401
|
-
* @param {string} requestId
|
|
1402
|
-
* @returns {RunnableEffect<HumanRequestRow | undefined, SmithersError>}
|
|
1403
|
-
*/
|
|
1404
|
-
getHumanRequest(requestId) {
|
|
1405
|
-
return this.read(`get human request ${requestId}`, () => this.internalStorage.queryOne(`SELECT *
|
|
1406
|
-
FROM _smithers_human_requests
|
|
1407
|
-
WHERE request_id = ?
|
|
1408
|
-
LIMIT 1`, [requestId]));
|
|
1409
|
-
}
|
|
1410
|
-
/**
|
|
1411
|
-
* @param {string} requestId
|
|
1412
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1413
|
-
*/
|
|
1414
|
-
reopenHumanRequest(requestId) {
|
|
1415
|
-
return this.write(`reopen human request ${requestId}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
|
|
1416
|
-
status: "pending",
|
|
1417
|
-
responseJson: null,
|
|
1418
|
-
answeredAtMs: null,
|
|
1419
|
-
answeredBy: null,
|
|
1420
|
-
}, "request_id = ? AND status = ?", [requestId, "answered"]));
|
|
1421
|
-
}
|
|
1422
|
-
/**
|
|
1423
|
-
* @param {number} [nowMs]
|
|
1424
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1425
|
-
*/
|
|
1426
|
-
expireStaleHumanRequests(nowMs = Date.now()) {
|
|
1427
|
-
return this.write(`expire stale human requests before ${nowMs}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
|
|
1428
|
-
status: "expired",
|
|
1429
|
-
responseJson: null,
|
|
1430
|
-
answeredAtMs: null,
|
|
1431
|
-
answeredBy: null,
|
|
1432
|
-
}, "status = ? AND timeout_at_ms IS NOT NULL AND timeout_at_ms <= ?", ["pending", nowMs]));
|
|
1433
|
-
}
|
|
1434
|
-
/**
|
|
1435
|
-
* @param {number} [nowMs]
|
|
1436
|
-
* @returns {RunnableEffect<PendingHumanRequestRow[], SmithersError>}
|
|
1437
|
-
*/
|
|
1438
|
-
listPendingHumanRequests(nowMs = Date.now()) {
|
|
1439
|
-
const self = this;
|
|
1440
|
-
return runnableEffect(Effect.gen(function* () {
|
|
1441
|
-
yield* self.expireStaleHumanRequests(nowMs);
|
|
1442
|
-
return yield* self.read("list pending human requests", () => self.internalStorage.queryAll(`SELECT
|
|
1443
|
-
h.request_id,
|
|
1444
|
-
h.run_id,
|
|
1445
|
-
h.node_id,
|
|
1446
|
-
h.iteration,
|
|
1447
|
-
h.kind,
|
|
1448
|
-
h.status,
|
|
1449
|
-
h.prompt,
|
|
1450
|
-
h.schema_json,
|
|
1451
|
-
h.options_json,
|
|
1452
|
-
h.response_json,
|
|
1453
|
-
h.requested_at_ms,
|
|
1454
|
-
h.answered_at_ms,
|
|
1455
|
-
h.answered_by,
|
|
1456
|
-
h.timeout_at_ms,
|
|
1457
|
-
r.workflow_name,
|
|
1458
|
-
r.status AS run_status,
|
|
1459
|
-
n.label AS node_label
|
|
1460
|
-
FROM _smithers_human_requests h
|
|
1461
|
-
LEFT JOIN _smithers_runs r ON h.run_id = r.run_id
|
|
1462
|
-
LEFT JOIN _smithers_nodes n
|
|
1463
|
-
ON h.run_id = n.run_id
|
|
1464
|
-
AND h.node_id = n.node_id
|
|
1465
|
-
AND h.iteration = n.iteration
|
|
1466
|
-
WHERE h.status = ?
|
|
1467
|
-
ORDER BY h.requested_at_ms ASC, h.run_id, h.node_id, h.iteration`, ["pending"]));
|
|
1468
|
-
}));
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* @param {string} requestId
|
|
1472
|
-
* @param {string} responseJson
|
|
1473
|
-
* @param {number} answeredAtMs
|
|
1474
|
-
* @param {string | null} [answeredBy]
|
|
1475
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1476
|
-
*/
|
|
1477
|
-
answerHumanRequest(requestId, responseJson, answeredAtMs, answeredBy) {
|
|
1478
|
-
return this.write(`answer human request ${requestId}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
|
|
1479
|
-
status: "answered",
|
|
1480
|
-
responseJson,
|
|
1481
|
-
answeredAtMs,
|
|
1482
|
-
answeredBy: answeredBy ?? null,
|
|
1483
|
-
}, "request_id = ? AND status = ?", [requestId, "pending"]));
|
|
1484
|
-
}
|
|
1485
|
-
/**
|
|
1486
|
-
* @param {string} requestId
|
|
1487
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1488
|
-
*/
|
|
1489
|
-
cancelHumanRequest(requestId) {
|
|
1490
|
-
return this.write(`cancel human request ${requestId}`, () => this.internalStorage.updateWhere("_smithers_human_requests", {
|
|
1491
|
-
status: "cancelled",
|
|
1492
|
-
}, "request_id = ? AND status = ?", [requestId, "pending"]));
|
|
1493
|
-
}
|
|
1494
|
-
/**
|
|
1495
|
-
* @param {AlertRow} row
|
|
1496
|
-
* @returns {Promise<AlertRow | undefined>}
|
|
1497
|
-
*/
|
|
1498
|
-
insertAlert(row) {
|
|
1499
|
-
validateAlertRow(row);
|
|
1500
|
-
const self = this;
|
|
1501
|
-
return this.withTransaction(`insert alert ${row.alertId}`, Effect.gen(function* () {
|
|
1502
|
-
const existing = yield* self.getAlert(row.alertId);
|
|
1503
|
-
if (existing) {
|
|
1504
|
-
return existing;
|
|
1505
|
-
}
|
|
1506
|
-
yield* self.write(`insert alert ${row.alertId}`, () => self.internalStorage.insertIgnore("_smithers_alerts", row));
|
|
1507
|
-
yield* Metric.increment(Metric.tagged(Metric.tagged(alertsFiredTotal, "policy", row.policyName), "severity", row.severity));
|
|
1508
|
-
if (isAlertActiveStatus(row.status)) {
|
|
1509
|
-
yield* Metric.update(alertsActive, 1);
|
|
1510
|
-
}
|
|
1511
|
-
return yield* self.getAlert(row.alertId);
|
|
1512
|
-
}));
|
|
1513
|
-
}
|
|
1514
|
-
/**
|
|
1515
|
-
* @param {string} alertId
|
|
1516
|
-
* @returns {RunnableEffect<AlertRow | undefined, SmithersError>}
|
|
1517
|
-
*/
|
|
1518
|
-
getAlert(alertId) {
|
|
1519
|
-
return this.read(`get alert ${alertId}`, () => this.internalStorage.queryOne(`SELECT *
|
|
1520
|
-
FROM _smithers_alerts
|
|
1521
|
-
WHERE alert_id = ?
|
|
1522
|
-
LIMIT 1`, [alertId]));
|
|
1523
|
-
}
|
|
1524
|
-
/**
|
|
1525
|
-
* @param {readonly AlertStatus[]} [statuses]
|
|
1526
|
-
* @returns {RunnableEffect<AlertRow[], SmithersError>}
|
|
1527
|
-
*/
|
|
1528
|
-
listAlerts(limit = 100, statuses) {
|
|
1529
|
-
if (statuses) {
|
|
1530
|
-
for (const status of statuses) {
|
|
1531
|
-
validateAlertStatus(status);
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
const normalizedLimit = Math.max(1, Math.floor(limit));
|
|
1535
|
-
return this.read("list alerts", () => {
|
|
1536
|
-
const clauses = [];
|
|
1537
|
-
const params = [];
|
|
1538
|
-
if (statuses && statuses.length > 0) {
|
|
1539
|
-
clauses.push(`status IN (${statuses.map(() => "?").join(", ")})`);
|
|
1540
|
-
params.push(...statuses);
|
|
1541
|
-
}
|
|
1542
|
-
const whereSql = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
1543
|
-
return this.internalStorage.queryAll(`SELECT *
|
|
1544
|
-
FROM _smithers_alerts
|
|
1545
|
-
${whereSql}
|
|
1546
|
-
ORDER BY
|
|
1547
|
-
CASE status
|
|
1548
|
-
WHEN 'firing' THEN 0
|
|
1549
|
-
WHEN 'acknowledged' THEN 1
|
|
1550
|
-
WHEN 'silenced' THEN 2
|
|
1551
|
-
WHEN 'resolved' THEN 3
|
|
1552
|
-
ELSE 4
|
|
1553
|
-
END,
|
|
1554
|
-
fired_at_ms DESC,
|
|
1555
|
-
alert_id ASC
|
|
1556
|
-
LIMIT ?`, [...params, normalizedLimit]);
|
|
1557
|
-
});
|
|
1558
|
-
}
|
|
1559
|
-
/**
|
|
1560
|
-
* @param {string} alertId
|
|
1561
|
-
* @returns {Promise<AlertRow | undefined>}
|
|
1562
|
-
*/
|
|
1563
|
-
acknowledgeAlert(alertId, acknowledgedAtMs = Date.now()) {
|
|
1564
|
-
validateOptionalPositiveTimestamp({ acknowledgedAtMs }, "acknowledgedAtMs");
|
|
1565
|
-
const self = this;
|
|
1566
|
-
return this.withTransaction(`acknowledge alert ${alertId}`, Effect.gen(function* () {
|
|
1567
|
-
const alert = yield* self.getAlert(alertId);
|
|
1568
|
-
if (!alert) {
|
|
1569
|
-
return undefined;
|
|
1570
|
-
}
|
|
1571
|
-
if (alert.status !== "firing") {
|
|
1572
|
-
return alert;
|
|
1573
|
-
}
|
|
1574
|
-
yield* self.write(`acknowledge alert ${alertId}`, () => self.internalStorage.updateWhere("_smithers_alerts", {
|
|
1575
|
-
status: "acknowledged",
|
|
1576
|
-
acknowledgedAtMs,
|
|
1577
|
-
}, "alert_id = ? AND status = ?", [alertId, "firing"]));
|
|
1578
|
-
yield* Metric.increment(Metric.tagged(alertsAcknowledgedTotal, "policy", alert.policyName));
|
|
1579
|
-
return yield* self.getAlert(alertId);
|
|
1580
|
-
}));
|
|
1581
|
-
}
|
|
1582
|
-
/**
|
|
1583
|
-
* @param {string} alertId
|
|
1584
|
-
* @returns {Promise<AlertRow | undefined>}
|
|
1585
|
-
*/
|
|
1586
|
-
resolveAlert(alertId, resolvedAtMs = Date.now()) {
|
|
1587
|
-
validateOptionalPositiveTimestamp({ resolvedAtMs }, "resolvedAtMs");
|
|
1588
|
-
const self = this;
|
|
1589
|
-
return this.withTransaction(`resolve alert ${alertId}`, Effect.gen(function* () {
|
|
1590
|
-
const alert = yield* self.getAlert(alertId);
|
|
1591
|
-
if (!alert) {
|
|
1592
|
-
return undefined;
|
|
1593
|
-
}
|
|
1594
|
-
if (alert.status === "resolved") {
|
|
1595
|
-
return alert;
|
|
1596
|
-
}
|
|
1597
|
-
yield* self.write(`resolve alert ${alertId}`, () => self.internalStorage.updateWhere("_smithers_alerts", {
|
|
1598
|
-
status: "resolved",
|
|
1599
|
-
resolvedAtMs,
|
|
1600
|
-
}, "alert_id = ? AND status != ?", [alertId, "resolved"]));
|
|
1601
|
-
if (isAlertActiveStatus(alert.status)) {
|
|
1602
|
-
yield* Metric.update(alertsActive, -1);
|
|
1603
|
-
}
|
|
1604
|
-
return yield* self.getAlert(alertId);
|
|
1605
|
-
}));
|
|
1606
|
-
}
|
|
1607
|
-
/**
|
|
1608
|
-
* @param {string} alertId
|
|
1609
|
-
* @returns {Promise<AlertRow | undefined>}
|
|
1610
|
-
*/
|
|
1611
|
-
silenceAlert(alertId) {
|
|
1612
|
-
const self = this;
|
|
1613
|
-
return this.withTransaction(`silence alert ${alertId}`, Effect.gen(function* () {
|
|
1614
|
-
const alert = yield* self.getAlert(alertId);
|
|
1615
|
-
if (!alert) {
|
|
1616
|
-
return undefined;
|
|
1617
|
-
}
|
|
1618
|
-
if (alert.status === "resolved" || alert.status === "silenced") {
|
|
1619
|
-
return alert;
|
|
1620
|
-
}
|
|
1621
|
-
yield* self.write(`silence alert ${alertId}`, () => self.internalStorage.updateWhere("_smithers_alerts", {
|
|
1622
|
-
status: "silenced",
|
|
1623
|
-
}, "alert_id = ? AND status != ? AND status != ?", [alertId, "resolved", "silenced"]));
|
|
1624
|
-
return yield* self.getAlert(alertId);
|
|
1625
|
-
}));
|
|
1626
|
-
}
|
|
1627
|
-
/**
|
|
1628
|
-
* @param {{ runId: string; signalName: string; correlationId: string | null; payloadJson: string; receivedAtMs: number; receivedBy?: string | null; }} row
|
|
1629
|
-
* @returns {RunnableEffect<number, SmithersError>}
|
|
1630
|
-
*/
|
|
1631
|
-
insertSignalWithNextSeq(row) {
|
|
1632
|
-
const label = `insert signal ${row.signalName}`;
|
|
1633
|
-
const self = this;
|
|
1634
|
-
return runnableEffect(withSqliteWriteRetryEffect(() => Effect.gen(function* () {
|
|
1635
|
-
const existing = yield* self.read(label, () => self.internalStorage.queryOne(`SELECT seq
|
|
1636
|
-
FROM _smithers_signals
|
|
1637
|
-
WHERE run_id = ?
|
|
1638
|
-
AND signal_name = ?
|
|
1639
|
-
AND ${row.correlationId === null ? "correlation_id IS NULL" : "correlation_id = ?"}
|
|
1640
|
-
AND payload_json = ?
|
|
1641
|
-
AND received_at_ms = ?
|
|
1642
|
-
AND ${row.receivedBy == null ? "received_by IS NULL" : "received_by = ?"}
|
|
1643
|
-
ORDER BY seq DESC
|
|
1644
|
-
LIMIT 1`, [
|
|
1645
|
-
row.runId,
|
|
1646
|
-
row.signalName,
|
|
1647
|
-
...(row.correlationId === null ? [] : [row.correlationId]),
|
|
1648
|
-
row.payloadJson,
|
|
1649
|
-
row.receivedAtMs,
|
|
1650
|
-
...(row.receivedBy == null ? [] : [row.receivedBy]),
|
|
1651
|
-
]));
|
|
1652
|
-
if (existing?.seq !== undefined) {
|
|
1653
|
-
return existing.seq;
|
|
1654
|
-
}
|
|
1655
|
-
const client = /** @type {{ exec?: unknown; query?: unknown; run?: unknown }} */ (resolveSqliteClientKey(self.db));
|
|
1656
|
-
if (typeof client.exec !== "function" ||
|
|
1657
|
-
typeof client.query !== "function" ||
|
|
1658
|
-
typeof client.run !== "function") {
|
|
1659
|
-
const lastSeq = (yield* self.getLastSignalSeq(row.runId)) ?? -1;
|
|
1660
|
-
const seq = lastSeq + 1;
|
|
1661
|
-
yield* Effect.tryPromise({
|
|
1662
|
-
try: () => self.internalStorage.insertIgnore("_smithers_signals", {
|
|
1663
|
-
...row,
|
|
1664
|
-
receivedBy: row.receivedBy ?? null,
|
|
1665
|
-
seq,
|
|
1666
|
-
}),
|
|
1667
|
-
catch: (cause) => toSmithersError(cause, "insert fallback signal row"),
|
|
1668
|
-
});
|
|
1669
|
-
return seq;
|
|
1670
|
-
}
|
|
1671
|
-
const releaseTurn = yield* self.acquireTransactionTurn();
|
|
1672
|
-
return yield* Effect.try({
|
|
1673
|
-
try: () => {
|
|
1674
|
-
client.run("BEGIN IMMEDIATE");
|
|
1675
|
-
try {
|
|
1676
|
-
const res = client
|
|
1677
|
-
.query("SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM _smithers_signals WHERE run_id = ?")
|
|
1678
|
-
.get(row.runId);
|
|
1679
|
-
const seq = Number(res?.seq ?? 0);
|
|
1680
|
-
client
|
|
1681
|
-
.query("INSERT INTO _smithers_signals (run_id, seq, signal_name, correlation_id, payload_json, received_at_ms, received_by) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
1682
|
-
.run(row.runId, seq, row.signalName, row.correlationId, row.payloadJson, row.receivedAtMs, row.receivedBy ?? null);
|
|
1683
|
-
client.run("COMMIT");
|
|
1684
|
-
return seq;
|
|
1685
|
-
}
|
|
1686
|
-
catch (error) {
|
|
1687
|
-
try {
|
|
1688
|
-
client.run("ROLLBACK");
|
|
1689
|
-
}
|
|
1690
|
-
catch {
|
|
1691
|
-
// ignore rollback failures
|
|
1692
|
-
}
|
|
1693
|
-
throw error;
|
|
1694
|
-
}
|
|
1695
|
-
},
|
|
1696
|
-
catch: (cause) => toSmithersError(cause, "insert signal transaction"),
|
|
1697
|
-
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
1698
|
-
releaseTurn();
|
|
1699
|
-
})));
|
|
1700
|
-
}), { label }).pipe(Effect.annotateLogs({
|
|
1701
|
-
runId: row.runId,
|
|
1702
|
-
signalName: row.signalName,
|
|
1703
|
-
correlationId: row.correlationId ?? null,
|
|
1704
|
-
}), Effect.withLogSpan(`db:${label}`)));
|
|
1705
|
-
}
|
|
1706
|
-
/**
|
|
1707
|
-
* @param {string} runId
|
|
1708
|
-
* @returns {RunnableEffect<number | undefined, SmithersError>}
|
|
1709
|
-
*/
|
|
1710
|
-
getLastSignalSeq(runId) {
|
|
1711
|
-
return this.read(`get last signal seq ${runId}`, () => this.internalStorage.getLastSignalSeq(runId));
|
|
1712
|
-
}
|
|
1713
|
-
/**
|
|
1714
|
-
* @param {string} runId
|
|
1715
|
-
* @param {SignalQuery} [query]
|
|
1716
|
-
* @returns {RunnableEffect<SignalRow[], SmithersError>}
|
|
1717
|
-
*/
|
|
1718
|
-
listSignals(runId, query = {}) {
|
|
1719
|
-
const limit = Math.max(1, Math.floor(query.limit ?? 200));
|
|
1720
|
-
return this.read(`list signals ${runId}`, () => {
|
|
1721
|
-
const clauses = ["run_id = ?"];
|
|
1722
|
-
const params = [runId];
|
|
1723
|
-
if (query.signalName) {
|
|
1724
|
-
clauses.push("signal_name = ?");
|
|
1725
|
-
params.push(query.signalName);
|
|
1726
|
-
}
|
|
1727
|
-
if (query.correlationId !== undefined) {
|
|
1728
|
-
if (query.correlationId === null) {
|
|
1729
|
-
clauses.push("correlation_id IS NULL");
|
|
1730
|
-
}
|
|
1731
|
-
else {
|
|
1732
|
-
clauses.push("correlation_id = ?");
|
|
1733
|
-
params.push(query.correlationId);
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
if (typeof query.receivedAfterMs === "number") {
|
|
1737
|
-
clauses.push("received_at_ms >= ?");
|
|
1738
|
-
params.push(query.receivedAfterMs);
|
|
1739
|
-
}
|
|
1740
|
-
return this.internalStorage.queryAll(`SELECT *
|
|
1741
|
-
FROM _smithers_signals
|
|
1742
|
-
WHERE ${clauses.join(" AND ")}
|
|
1743
|
-
ORDER BY seq ASC
|
|
1744
|
-
LIMIT ?`, [...params, limit]);
|
|
1745
|
-
});
|
|
1746
|
-
}
|
|
1747
|
-
/**
|
|
1748
|
-
* @param {Record<string, unknown>} row
|
|
1749
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1750
|
-
*/
|
|
1751
|
-
insertToolCall(row) {
|
|
1752
|
-
return this.write(`insert tool call ${row.toolName}`, () => this.internalStorage.insertIgnore("_smithers_tool_calls", row));
|
|
1753
|
-
}
|
|
1754
|
-
/**
|
|
1755
|
-
* @param {Record<string, unknown>} row
|
|
1756
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1757
|
-
*/
|
|
1758
|
-
upsertSandbox(row) {
|
|
1759
|
-
return this.write(`upsert sandbox ${row.sandboxId}`, () => this.internalStorage.upsert("_smithers_sandboxes", row, ["runId", "sandboxId"]));
|
|
1760
|
-
}
|
|
1761
|
-
/**
|
|
1762
|
-
* @param {string} runId
|
|
1763
|
-
* @param {string} sandboxId
|
|
1764
|
-
* @returns {RunnableEffect<Record<string, unknown> | undefined, SmithersError>}
|
|
1765
|
-
*/
|
|
1766
|
-
getSandbox(runId, sandboxId) {
|
|
1767
|
-
return this.read(`get sandbox ${sandboxId}`, () => this.internalStorage.queryOne(`SELECT *
|
|
1768
|
-
FROM _smithers_sandboxes
|
|
1769
|
-
WHERE run_id = ? AND sandbox_id = ?
|
|
1770
|
-
LIMIT 1`, [runId, sandboxId]));
|
|
1771
|
-
}
|
|
1772
|
-
/**
|
|
1773
|
-
* @param {string} runId
|
|
1774
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1775
|
-
*/
|
|
1776
|
-
listSandboxes(runId) {
|
|
1777
|
-
return this.read(`list sandboxes ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1778
|
-
FROM _smithers_sandboxes
|
|
1779
|
-
WHERE run_id = ?`, [runId]));
|
|
1780
|
-
}
|
|
1781
|
-
/**
|
|
1782
|
-
* @param {string} runId
|
|
1783
|
-
* @param {string} nodeId
|
|
1784
|
-
* @param {number} iteration
|
|
1785
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1786
|
-
*/
|
|
1787
|
-
listToolCalls(runId, nodeId, iteration) {
|
|
1788
|
-
return this.read(`list tool calls ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1789
|
-
FROM _smithers_tool_calls
|
|
1790
|
-
WHERE run_id = ? AND node_id = ? AND iteration = ?
|
|
1791
|
-
ORDER BY attempt ASC, seq ASC`, [runId, nodeId, iteration]));
|
|
1792
|
-
}
|
|
1793
|
-
/**
|
|
1794
|
-
* @param {Record<string, unknown>} row
|
|
1795
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1796
|
-
*/
|
|
1797
|
-
insertEvent(row) {
|
|
1798
|
-
return this.write(`insert event ${row.type}`, () => this.internalStorage.insertIgnore("_smithers_events", row));
|
|
1799
|
-
}
|
|
1800
|
-
/**
|
|
1801
|
-
* @param {{ runId: string; timestampMs: number; type: string; payloadJson: string; }} row
|
|
1802
|
-
* @returns {RunnableEffect<number, SmithersError>}
|
|
1803
|
-
*/
|
|
1804
|
-
insertEventWithNextSeq(row) {
|
|
1805
|
-
const label = `insert event ${row.type}`;
|
|
1806
|
-
const self = this;
|
|
1807
|
-
return runnableEffect(withSqliteWriteRetryEffect(() => Effect.gen(function* () {
|
|
1808
|
-
const existing = yield* self.read(label, () => self.internalStorage.queryOne(`SELECT seq
|
|
1809
|
-
FROM _smithers_events
|
|
1810
|
-
WHERE run_id = ?
|
|
1811
|
-
AND timestamp_ms = ?
|
|
1812
|
-
AND type = ?
|
|
1813
|
-
AND payload_json = ?
|
|
1814
|
-
ORDER BY seq DESC
|
|
1815
|
-
LIMIT 1`, [row.runId, row.timestampMs, row.type, row.payloadJson]));
|
|
1816
|
-
if (existing?.seq !== undefined) {
|
|
1817
|
-
return existing.seq;
|
|
1818
|
-
}
|
|
1819
|
-
const client = /** @type {{ exec?: unknown; query?: unknown; run?: unknown }} */ (resolveSqliteClientKey(self.db));
|
|
1820
|
-
if (typeof client.exec !== "function" ||
|
|
1821
|
-
typeof client.query !== "function" ||
|
|
1822
|
-
typeof client.run !== "function") {
|
|
1823
|
-
const lastSeq = (yield* self.getLastEventSeq(row.runId)) ?? -1;
|
|
1824
|
-
const seq = lastSeq + 1;
|
|
1825
|
-
yield* Effect.tryPromise({
|
|
1826
|
-
try: () => self.internalStorage.insertIgnore("_smithers_events", { ...row, seq }),
|
|
1827
|
-
catch: (cause) => toSmithersError(cause, "insert fallback event row"),
|
|
1828
|
-
});
|
|
1829
|
-
return seq;
|
|
1830
|
-
}
|
|
1831
|
-
const releaseTurn = yield* self.acquireTransactionTurn();
|
|
1832
|
-
return yield* Effect.try({
|
|
1833
|
-
try: () => {
|
|
1834
|
-
client.run("BEGIN IMMEDIATE");
|
|
1835
|
-
try {
|
|
1836
|
-
const res = client
|
|
1837
|
-
.query("SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM _smithers_events WHERE run_id = ?")
|
|
1838
|
-
.get(row.runId);
|
|
1839
|
-
const seq = Number(res?.seq ?? 0);
|
|
1840
|
-
client
|
|
1841
|
-
.query("INSERT INTO _smithers_events (run_id, seq, timestamp_ms, type, payload_json) VALUES (?, ?, ?, ?, ?)")
|
|
1842
|
-
.run(row.runId, seq, row.timestampMs, row.type, row.payloadJson);
|
|
1843
|
-
client.run("COMMIT");
|
|
1844
|
-
return seq;
|
|
1845
|
-
}
|
|
1846
|
-
catch (error) {
|
|
1847
|
-
try {
|
|
1848
|
-
client.run("ROLLBACK");
|
|
1849
|
-
}
|
|
1850
|
-
catch {
|
|
1851
|
-
// ignore rollback failures
|
|
1852
|
-
}
|
|
1853
|
-
throw error;
|
|
1854
|
-
}
|
|
1855
|
-
},
|
|
1856
|
-
catch: (cause) => toSmithersError(cause, "insert event transaction"),
|
|
1857
|
-
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
1858
|
-
releaseTurn();
|
|
1859
|
-
})));
|
|
1860
|
-
}), { label }).pipe(Effect.annotateLogs({ dbOperation: label }), Effect.withLogSpan(`db:${label}`)));
|
|
1861
|
-
}
|
|
1862
|
-
/**
|
|
1863
|
-
* @param {string} runId
|
|
1864
|
-
* @returns {RunnableEffect<number | undefined, SmithersError>}
|
|
1865
|
-
*/
|
|
1866
|
-
getLastEventSeq(runId) {
|
|
1867
|
-
return this.read(`get last event seq ${runId}`, () => this.internalStorage.getLastEventSeq(runId));
|
|
1868
|
-
}
|
|
1869
|
-
/**
|
|
1870
|
-
* @param {string} runId
|
|
1871
|
-
* @param {EventHistoryQuery} [query]
|
|
1872
|
-
* @returns {{ whereSql: string; params: Array<string | number> }}
|
|
1873
|
-
*/
|
|
1874
|
-
buildEventHistoryWhere(runId, query = {}) {
|
|
1875
|
-
const clauses = ["run_id = ?", "seq > ?"];
|
|
1876
|
-
const params = [runId, query.afterSeq ?? -1];
|
|
1877
|
-
if (typeof query.sinceTimestampMs === "number") {
|
|
1878
|
-
clauses.push("timestamp_ms >= ?");
|
|
1879
|
-
params.push(query.sinceTimestampMs);
|
|
1880
|
-
}
|
|
1881
|
-
if (query.types && query.types.length > 0) {
|
|
1882
|
-
clauses.push(`type IN (${query.types.map(() => "?").join(", ")})`);
|
|
1883
|
-
params.push(...query.types);
|
|
1884
|
-
}
|
|
1885
|
-
if (query.nodeId) {
|
|
1886
|
-
clauses.push("json_extract(payload_json, '$.nodeId') = ?");
|
|
1887
|
-
params.push(query.nodeId);
|
|
1888
|
-
}
|
|
1889
|
-
return {
|
|
1890
|
-
whereSql: clauses.join(" AND "),
|
|
1891
|
-
params,
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
/**
|
|
1895
|
-
* @param {string} runId
|
|
1896
|
-
* @param {EventHistoryQuery} [query]
|
|
1897
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1898
|
-
*/
|
|
1899
|
-
listEventHistory(runId, query = {}) {
|
|
1900
|
-
return this.read(`list event history ${runId}`, () => this.internalStorage.listEventHistory(runId, query));
|
|
1901
|
-
}
|
|
1902
|
-
/**
|
|
1903
|
-
* @param {string} runId
|
|
1904
|
-
* @param {EventHistoryQuery} [query]
|
|
1905
|
-
* @returns {RunnableEffect<number, SmithersError>}
|
|
1906
|
-
*/
|
|
1907
|
-
countEventHistory(runId, query = {}) {
|
|
1908
|
-
return this.read(`count event history ${runId}`, () => this.internalStorage.countEventHistory(runId, query));
|
|
1909
|
-
}
|
|
1910
|
-
/**
|
|
1911
|
-
* @param {string} runId
|
|
1912
|
-
* @param {number} afterSeq
|
|
1913
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1914
|
-
*/
|
|
1915
|
-
listEvents(runId, afterSeq, limit = 200) {
|
|
1916
|
-
return this.listEventHistory(runId, { afterSeq, limit });
|
|
1917
|
-
}
|
|
1918
|
-
/**
|
|
1919
|
-
* @param {string} runId
|
|
1920
|
-
* @param {string} type
|
|
1921
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1922
|
-
*/
|
|
1923
|
-
listEventsByType(runId, type) {
|
|
1924
|
-
return this.read(`list events by type ${type}`, () => this.internalStorage.listEventsByType(runId, type));
|
|
1925
|
-
}
|
|
1926
|
-
/**
|
|
1927
|
-
* @param {Record<string, unknown>} row
|
|
1928
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
1929
|
-
*/
|
|
1930
|
-
insertOrUpdateRalph(row) {
|
|
1931
|
-
return this.write(`upsert ralph ${row.ralphId}`, () => this.internalStorage.upsert("_smithers_ralph", row, ["runId", "ralphId"]));
|
|
1932
|
-
}
|
|
1933
|
-
/**
|
|
1934
|
-
* @param {string} runId
|
|
1935
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1936
|
-
*/
|
|
1937
|
-
listRalph(runId) {
|
|
1938
|
-
return this.read(`list ralph ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1939
|
-
FROM _smithers_ralph
|
|
1940
|
-
WHERE run_id = ?`, [runId], { booleanColumns: ["done"] }));
|
|
1941
|
-
}
|
|
1942
|
-
/**
|
|
1943
|
-
* @param {string} runId
|
|
1944
|
-
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
1945
|
-
*/
|
|
1946
|
-
listPendingApprovals(runId) {
|
|
1947
|
-
return this.read(`list pending approvals ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
1948
|
-
FROM _smithers_approvals
|
|
1949
|
-
WHERE run_id = ? AND status = ?`, [runId, "requested"], { booleanColumns: ["autoApproved"] }));
|
|
1950
|
-
}
|
|
1951
|
-
/**
|
|
1952
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1953
|
-
*/
|
|
1954
|
-
listAllPendingApprovals() {
|
|
1955
|
-
return this.read("list all pending approvals", () => this.internalStorage.queryAll(`SELECT
|
|
1956
|
-
a.run_id,
|
|
1957
|
-
a.node_id,
|
|
1958
|
-
a.iteration,
|
|
1959
|
-
a.status,
|
|
1960
|
-
a.requested_at_ms,
|
|
1961
|
-
a.note,
|
|
1962
|
-
a.decided_by,
|
|
1963
|
-
r.workflow_name,
|
|
1964
|
-
r.status AS run_status,
|
|
1965
|
-
n.label AS node_label
|
|
1966
|
-
FROM _smithers_approvals a
|
|
1967
|
-
LEFT JOIN _smithers_runs r ON a.run_id = r.run_id
|
|
1968
|
-
LEFT JOIN _smithers_nodes n
|
|
1969
|
-
ON a.run_id = n.run_id
|
|
1970
|
-
AND a.node_id = n.node_id
|
|
1971
|
-
AND a.iteration = n.iteration
|
|
1972
|
-
WHERE a.status = ?
|
|
1973
|
-
ORDER BY COALESCE(a.requested_at_ms, 0) ASC, a.run_id, a.node_id, a.iteration`, ["requested"]));
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* @param {string} workflowName
|
|
1977
|
-
* @param {string} nodeId
|
|
1978
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
1979
|
-
*/
|
|
1980
|
-
listApprovalHistoryForNode(workflowName, nodeId, limit = 50) {
|
|
1981
|
-
return this.read(`list approval history ${workflowName}:${nodeId}`, () => this.internalStorage.queryAll(`SELECT
|
|
1982
|
-
a.run_id,
|
|
1983
|
-
a.node_id,
|
|
1984
|
-
a.iteration,
|
|
1985
|
-
a.status,
|
|
1986
|
-
a.requested_at_ms,
|
|
1987
|
-
a.decided_at_ms,
|
|
1988
|
-
a.note,
|
|
1989
|
-
a.decided_by,
|
|
1990
|
-
a.request_json,
|
|
1991
|
-
a.decision_json,
|
|
1992
|
-
a.auto_approved,
|
|
1993
|
-
r.workflow_name,
|
|
1994
|
-
r.created_at_ms AS run_created_at_ms
|
|
1995
|
-
FROM _smithers_approvals a
|
|
1996
|
-
INNER JOIN _smithers_runs r ON a.run_id = r.run_id
|
|
1997
|
-
WHERE r.workflow_name = ? AND a.node_id = ?
|
|
1998
|
-
ORDER BY r.created_at_ms DESC, a.decided_at_ms DESC
|
|
1999
|
-
LIMIT ?`, [workflowName, nodeId, limit], { booleanColumns: ["autoApproved"] }));
|
|
2000
|
-
}
|
|
2001
|
-
/**
|
|
2002
|
-
* @param {string} runId
|
|
2003
|
-
* @param {string} ralphId
|
|
2004
|
-
* @returns {RunnableEffect<Record<string, unknown> | undefined, SmithersError>}
|
|
2005
|
-
*/
|
|
2006
|
-
getRalph(runId, ralphId) {
|
|
2007
|
-
return this.read(`get ralph ${ralphId}`, () => this.internalStorage.queryOne(`SELECT *
|
|
2008
|
-
FROM _smithers_ralph
|
|
2009
|
-
WHERE run_id = ? AND ralph_id = ?
|
|
2010
|
-
LIMIT 1`, [runId, ralphId], { booleanColumns: ["done"] }));
|
|
2011
|
-
}
|
|
2012
|
-
/**
|
|
2013
|
-
* @param {Record<string, unknown>} row
|
|
2014
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2015
|
-
*/
|
|
2016
|
-
insertCache(row) {
|
|
2017
|
-
return this.write(`insert cache ${row.cacheKey}`, () => this.internalStorage.insertIgnore("_smithers_cache", row));
|
|
2018
|
-
}
|
|
2019
|
-
/**
|
|
2020
|
-
* @param {Record<string, unknown>} row
|
|
2021
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2022
|
-
*/
|
|
2023
|
-
insertCacheEffect(row) {
|
|
2024
|
-
return this.insertCache(row);
|
|
2025
|
-
}
|
|
2026
|
-
/**
|
|
2027
|
-
* @param {string} cacheKey
|
|
2028
|
-
* @returns {RunnableEffect<CacheRow | undefined, SmithersError>}
|
|
2029
|
-
*/
|
|
2030
|
-
getCache(cacheKey) {
|
|
2031
|
-
return this.read(`get cache ${cacheKey}`, () => this.internalStorage.queryOne(`SELECT *
|
|
2032
|
-
FROM _smithers_cache
|
|
2033
|
-
WHERE cache_key = ?
|
|
2034
|
-
LIMIT 1`, [cacheKey]));
|
|
2035
|
-
}
|
|
2036
|
-
/**
|
|
2037
|
-
* @param {string} nodeId
|
|
2038
|
-
* @param {string} [outputTable]
|
|
2039
|
-
* @returns {RunnableEffect<CacheRow[], SmithersError>}
|
|
2040
|
-
*/
|
|
2041
|
-
listCacheByNode(nodeId, outputTable, limit = 20) {
|
|
2042
|
-
return this.read(`list cache by node ${nodeId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
2043
|
-
FROM _smithers_cache
|
|
2044
|
-
WHERE node_id = ?${outputTable ? " AND output_table = ?" : ""}
|
|
2045
|
-
ORDER BY created_at_ms DESC
|
|
2046
|
-
LIMIT ?`, outputTable ? [nodeId, outputTable, limit] : [nodeId, limit]));
|
|
2047
|
-
}
|
|
2048
|
-
/**
|
|
2049
|
-
* @param {string} runId
|
|
2050
|
-
* @param {number} frameNo
|
|
2051
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2052
|
-
*/
|
|
2053
|
-
deleteFramesAfter(runId, frameNo) {
|
|
2054
|
-
const self = this;
|
|
2055
|
-
return runnableEffect(Effect.gen(function* () {
|
|
2056
|
-
yield* self.write(`delete frames after ${frameNo}`, () => self.internalStorage.deleteWhere("_smithers_frames", "run_id = ? AND frame_no > ?", [runId, frameNo]));
|
|
2057
|
-
self.clearFrameCacheForRun(runId);
|
|
2058
|
-
}));
|
|
2059
|
-
}
|
|
2060
|
-
/**
|
|
2061
|
-
* @param {string} runId
|
|
2062
|
-
* @param {number} limit
|
|
2063
|
-
* @param {number} [afterFrameNo]
|
|
2064
|
-
* @returns {RunnableEffect<FrameRow[], SmithersError>}
|
|
2065
|
-
*/
|
|
2066
|
-
listFrames(runId, limit, afterFrameNo) {
|
|
2067
|
-
const self = this;
|
|
2068
|
-
return runnableEffect(Effect.gen(function* () {
|
|
2069
|
-
const rows = (yield* self.read(`list frames ${runId}`, () => self.internalStorage.queryAll(`SELECT *
|
|
2070
|
-
FROM _smithers_frames
|
|
2071
|
-
WHERE run_id = ?${afterFrameNo !== undefined ? " AND frame_no > ?" : ""}
|
|
2072
|
-
ORDER BY frame_no DESC
|
|
2073
|
-
LIMIT ?`, afterFrameNo !== undefined
|
|
2074
|
-
? [runId, afterFrameNo, limit]
|
|
2075
|
-
: [runId, limit])));
|
|
2076
|
-
const localCache = new Map();
|
|
2077
|
-
const expanded = [];
|
|
2078
|
-
for (const row of rows) {
|
|
2079
|
-
expanded.push(yield* self.inflateFrameRow(row, localCache));
|
|
2080
|
-
}
|
|
2081
|
-
return expanded;
|
|
2082
|
-
}));
|
|
2083
|
-
}
|
|
2084
|
-
/**
|
|
2085
|
-
* @param {string} runId
|
|
2086
|
-
* @returns {RunnableEffect<Array<{ state: string; count: number }>, SmithersError>}
|
|
2087
|
-
*/
|
|
2088
|
-
countNodesByState(runId) {
|
|
2089
|
-
return this.read(`count nodes by state ${runId}`, () => this.internalStorage.queryAll(`SELECT state, COUNT(*) AS count
|
|
2090
|
-
FROM _smithers_nodes
|
|
2091
|
-
WHERE run_id = ?
|
|
2092
|
-
GROUP BY state`, [runId]));
|
|
2093
|
-
}
|
|
2094
|
-
/**
|
|
2095
|
-
* @param {Record<string, unknown>} row
|
|
2096
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2097
|
-
*/
|
|
2098
|
-
upsertCron(row) {
|
|
2099
|
-
return this.write("upsert cron", () => this.internalStorage.upsert("_smithers_cron", row, ["cronId"], ["pattern", "workflowPath", "enabled", "nextRunAtMs"]));
|
|
2100
|
-
}
|
|
2101
|
-
/**
|
|
2102
|
-
* @param {boolean} [enabledOnly]
|
|
2103
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2104
|
-
*/
|
|
2105
|
-
listCrons(enabledOnly = true) {
|
|
2106
|
-
return this.read("list crons", () => this.internalStorage.queryAll(`SELECT *
|
|
2107
|
-
FROM _smithers_cron${enabledOnly ? " WHERE enabled = ?" : ""}`, enabledOnly ? [true] : [], { booleanColumns: ["enabled"] }));
|
|
2108
|
-
}
|
|
2109
|
-
/**
|
|
2110
|
-
* @param {string} cronId
|
|
2111
|
-
* @param {number} lastRunAtMs
|
|
2112
|
-
* @param {number} nextRunAtMs
|
|
2113
|
-
* @param {string | null} [errorJson]
|
|
2114
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2115
|
-
*/
|
|
2116
|
-
updateCronRunTime(cronId, lastRunAtMs, nextRunAtMs, errorJson) {
|
|
2117
|
-
return this.write(`update cron run time ${cronId}`, () => this.internalStorage.updateWhere("_smithers_cron", { lastRunAtMs, nextRunAtMs, errorJson: errorJson ?? null }, "cron_id = ?", [cronId]));
|
|
2118
|
-
}
|
|
2119
|
-
/**
|
|
2120
|
-
* @param {string} cronId
|
|
2121
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2122
|
-
*/
|
|
2123
|
-
deleteCron(cronId) {
|
|
2124
|
-
return this.write(`delete cron ${cronId}`, () => this.internalStorage.deleteWhere("_smithers_cron", "cron_id = ?", [cronId]));
|
|
2125
|
-
}
|
|
2126
|
-
// ---------------------------------------------------------------------------
|
|
2127
|
-
// Scorer results
|
|
2128
|
-
// ---------------------------------------------------------------------------
|
|
2129
|
-
/**
|
|
2130
|
-
* @param {Record<string, unknown>} row
|
|
2131
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2132
|
-
*/
|
|
2133
|
-
insertScorerResult(row) {
|
|
2134
|
-
return this.write(`insert scorer result ${row.scorerId}`, () => this.internalStorage.insertIgnore("_smithers_scorers", row));
|
|
2135
|
-
}
|
|
2136
|
-
/**
|
|
2137
|
-
* @param {string} runId
|
|
2138
|
-
* @param {string} [nodeId]
|
|
2139
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2140
|
-
*/
|
|
2141
|
-
listScorerResults(runId, nodeId) {
|
|
2142
|
-
return this.read(`list scorer results ${runId}`, () => this.internalStorage.queryAll(`SELECT *
|
|
2143
|
-
FROM _smithers_scorers
|
|
2144
|
-
WHERE run_id = ?${nodeId ? " AND node_id = ?" : ""}
|
|
2145
|
-
ORDER BY scored_at_ms ASC`, nodeId ? [runId, nodeId] : [runId]));
|
|
2146
|
-
}
|
|
2147
|
-
/**
|
|
2148
|
-
* @param {string} runId
|
|
2149
|
-
* @returns {RunnableEffect<RunRow | undefined, SmithersError>}
|
|
2150
|
-
*/
|
|
2151
|
-
getRunEffect(runId) {
|
|
2152
|
-
return this.getRun(runId);
|
|
2153
|
-
}
|
|
2154
|
-
/**
|
|
2155
|
-
* @param {string} [status]
|
|
2156
|
-
* @returns {RunnableEffect<RunRow[], SmithersError>}
|
|
2157
|
-
*/
|
|
2158
|
-
listRunsEffect(limit = 50, status) {
|
|
2159
|
-
return this.listRuns(limit, status);
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* @param {number} staleBeforeMs
|
|
2163
|
-
* @returns {RunnableEffect<StaleRunRecord[], SmithersError>}
|
|
2164
|
-
*/
|
|
2165
|
-
listStaleRunningRunsEffect(staleBeforeMs, limit = 1000) {
|
|
2166
|
-
return this.listStaleRunningRuns(staleBeforeMs, limit);
|
|
2167
|
-
}
|
|
2168
|
-
/**
|
|
2169
|
-
* @param {Parameters<SmithersDb["claimRunForResume"]>[0]} params
|
|
2170
|
-
* @returns {RunnableEffect<boolean, SmithersError>}
|
|
2171
|
-
*/
|
|
2172
|
-
claimRunForResumeEffect(params) {
|
|
2173
|
-
return this.claimRunForResume(params);
|
|
2174
|
-
}
|
|
2175
|
-
/**
|
|
2176
|
-
* @param {Parameters<SmithersDb["releaseRunResumeClaim"]>[0]} params
|
|
2177
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2178
|
-
*/
|
|
2179
|
-
releaseRunResumeClaimEffect(params) {
|
|
2180
|
-
return this.releaseRunResumeClaim(params);
|
|
2181
|
-
}
|
|
2182
|
-
/**
|
|
2183
|
-
* @param {string} runId
|
|
2184
|
-
* @param {string} nodeId
|
|
2185
|
-
* @returns {RunnableEffect<NodeRow[], SmithersError>}
|
|
2186
|
-
*/
|
|
2187
|
-
listNodeIterationsEffect(runId, nodeId) {
|
|
2188
|
-
return this.listNodeIterations(runId, nodeId);
|
|
2189
|
-
}
|
|
2190
|
-
/**
|
|
2191
|
-
* @param {string} runId
|
|
2192
|
-
* @returns {RunnableEffect<NodeRow[], SmithersError>}
|
|
2193
|
-
*/
|
|
2194
|
-
listNodesEffect(runId) {
|
|
2195
|
-
return this.listNodes(runId);
|
|
2196
|
-
}
|
|
2197
|
-
/**
|
|
2198
|
-
* @param {string} runId
|
|
2199
|
-
* @param {string} nodeId
|
|
2200
|
-
* @param {number} iteration
|
|
2201
|
-
* @returns {RunnableEffect<AttemptRow[], SmithersError>}
|
|
2202
|
-
*/
|
|
2203
|
-
listAttemptsEffect(runId, nodeId, iteration) {
|
|
2204
|
-
return this.listAttempts(runId, nodeId, iteration);
|
|
2205
|
-
}
|
|
2206
|
-
/**
|
|
2207
|
-
* @param {string} runId
|
|
2208
|
-
* @returns {RunnableEffect<AttemptRow[], SmithersError>}
|
|
2209
|
-
*/
|
|
2210
|
-
listAttemptsForRunEffect(runId) {
|
|
2211
|
-
return this.listAttemptsForRun(runId);
|
|
2212
|
-
}
|
|
2213
|
-
/**
|
|
2214
|
-
* @param {string} runId
|
|
2215
|
-
* @param {string} nodeId
|
|
2216
|
-
* @param {number} iteration
|
|
2217
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2218
|
-
*/
|
|
2219
|
-
listToolCallsEffect(runId, nodeId, iteration) {
|
|
2220
|
-
return this.listToolCalls(runId, nodeId, iteration);
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* @param {string} tableName
|
|
2224
|
-
* @param {string} runId
|
|
2225
|
-
* @param {string} nodeId
|
|
2226
|
-
* @param {number} iteration
|
|
2227
|
-
* @returns {RunnableEffect<Record<string, unknown> | null, SmithersError>}
|
|
2228
|
-
*/
|
|
2229
|
-
getRawNodeOutputForIterationEffect(tableName, runId, nodeId, iteration) {
|
|
2230
|
-
return this.getRawNodeOutputForIteration(tableName, runId, nodeId, iteration);
|
|
2231
|
-
}
|
|
2232
|
-
/**
|
|
2233
|
-
* @param {Parameters<SmithersDb["insertEventWithNextSeq"]>[0]} row
|
|
2234
|
-
* @returns {RunnableEffect<number, SmithersError>}
|
|
2235
|
-
*/
|
|
2236
|
-
insertEventWithNextSeqEffect(row) {
|
|
2237
|
-
return this.insertEventWithNextSeq(row);
|
|
2238
|
-
}
|
|
2239
|
-
/**
|
|
2240
|
-
* @param {string} runId
|
|
2241
|
-
* @returns {RunnableEffect<number | undefined, SmithersError>}
|
|
2242
|
-
*/
|
|
2243
|
-
getLastEventSeqEffect(runId) {
|
|
2244
|
-
return this.getLastEventSeq(runId);
|
|
2245
|
-
}
|
|
2246
|
-
/**
|
|
2247
|
-
* @param {string} runId
|
|
2248
|
-
* @param {EventHistoryQuery} [query]
|
|
2249
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2250
|
-
*/
|
|
2251
|
-
listEventHistoryEffect(runId, query = {}) {
|
|
2252
|
-
return this.listEventHistory(runId, query);
|
|
2253
|
-
}
|
|
2254
|
-
/**
|
|
2255
|
-
* @param {string} runId
|
|
2256
|
-
* @param {EventHistoryQuery} [query]
|
|
2257
|
-
* @returns {RunnableEffect<number, SmithersError>}
|
|
2258
|
-
*/
|
|
2259
|
-
countEventHistoryEffect(runId, query = {}) {
|
|
2260
|
-
return this.countEventHistory(runId, query);
|
|
2261
|
-
}
|
|
2262
|
-
/**
|
|
2263
|
-
* @param {string} runId
|
|
2264
|
-
* @param {string} type
|
|
2265
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2266
|
-
*/
|
|
2267
|
-
listEventsByTypeEffect(runId, type) {
|
|
2268
|
-
return this.listEventsByType(runId, type);
|
|
2269
|
-
}
|
|
2270
|
-
/**
|
|
2271
|
-
* @param {string} runId
|
|
2272
|
-
* @returns {RunnableEffect<ApprovalRow[], SmithersError>}
|
|
2273
|
-
*/
|
|
2274
|
-
listPendingApprovalsEffect(runId) {
|
|
2275
|
-
return this.listPendingApprovals(runId);
|
|
2276
|
-
}
|
|
2277
|
-
/**
|
|
2278
|
-
* @param {string} runId
|
|
2279
|
-
* @returns {RunnableEffect<FrameRow | undefined, SmithersError>}
|
|
2280
|
-
*/
|
|
2281
|
-
getLastFrameEffect(runId) {
|
|
2282
|
-
return this.getLastFrame(runId);
|
|
2283
|
-
}
|
|
2284
|
-
/**
|
|
2285
|
-
* @param {string} nodeId
|
|
2286
|
-
* @param {string} [outputTable]
|
|
2287
|
-
* @returns {RunnableEffect<CacheRow[], SmithersError>}
|
|
2288
|
-
*/
|
|
2289
|
-
listCacheByNodeEffect(nodeId, outputTable, limit = 20) {
|
|
2290
|
-
return this.listCacheByNode(nodeId, outputTable, limit);
|
|
2291
|
-
}
|
|
2292
|
-
/**
|
|
2293
|
-
* @param {boolean} [enabledOnly]
|
|
2294
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2295
|
-
*/
|
|
2296
|
-
listCronsEffect(enabledOnly = true) {
|
|
2297
|
-
return this.listCrons(enabledOnly);
|
|
2298
|
-
}
|
|
2299
|
-
/**
|
|
2300
|
-
* @param {string} cronId
|
|
2301
|
-
* @param {number} lastRunAtMs
|
|
2302
|
-
* @param {number} nextRunAtMs
|
|
2303
|
-
* @param {string | null} [errorJson]
|
|
2304
|
-
* @returns {RunnableEffect<void, SmithersError>}
|
|
2305
|
-
*/
|
|
2306
|
-
updateCronRunTimeEffect(cronId, lastRunAtMs, nextRunAtMs, errorJson) {
|
|
2307
|
-
return this.updateCronRunTime(cronId, lastRunAtMs, nextRunAtMs, errorJson);
|
|
2308
|
-
}
|
|
2309
|
-
/**
|
|
2310
|
-
* @param {string} runId
|
|
2311
|
-
* @param {string} [nodeId]
|
|
2312
|
-
* @returns {RunnableEffect<Array<Record<string, unknown>>, SmithersError>}
|
|
2313
|
-
*/
|
|
2314
|
-
listScorerResultsEffect(runId, nodeId) {
|
|
2315
|
-
return this.listScorerResults(runId, nodeId);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
1
|
+
// Keep the historical subpath import stable without maintaining a second copy
|
|
2
|
+
// of the adapter implementation.
|
|
3
|
+
export { SmithersDb } from "../adapter.js";
|