@smithers-orchestrator/db 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +43 -0
- package/src/JsonBounds.ts +6 -0
- package/src/SchemaRegistryEntry.ts +6 -0
- package/src/SqlMessageStorage.js +818 -0
- package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
- package/src/SqliteWriteRetryOptions.ts +7 -0
- package/src/adapter/AlertRow.ts +29 -0
- package/src/adapter/AlertSeverity.ts +2 -0
- package/src/adapter/AlertStatus.ts +2 -0
- package/src/adapter/ApprovalRow.ts +13 -0
- package/src/adapter/AttemptRow.ts +17 -0
- package/src/adapter/CacheRow.ts +12 -0
- package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
- package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
- package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
- package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
- package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
- package/src/adapter/EventHistoryQuery.ts +7 -0
- package/src/adapter/HumanRequestRow.ts +19 -0
- package/src/adapter/NodeDiffCacheRow.ts +9 -0
- package/src/adapter/NodeRow.ts +10 -0
- package/src/adapter/PendingHumanRequestRow.ts +7 -0
- package/src/adapter/RunAncestryRow.ts +5 -0
- package/src/adapter/RunRow.ts +21 -0
- package/src/adapter/SignalQuery.ts +6 -0
- package/src/adapter/SignalRow.ts +9 -0
- package/src/adapter/SmithersDb.js +2236 -0
- package/src/adapter/StaleRunRecord.ts +7 -0
- package/src/adapter/index.js +27 -0
- package/src/adapter.js +2359 -0
- package/src/assertJsonPayloadWithinBounds.js +94 -0
- package/src/assertMaxBytes.js +23 -0
- package/src/assertMaxJsonDepth.js +40 -0
- package/src/assertMaxStringLength.js +16 -0
- package/src/assertOptionalArrayMaxLength.js +16 -0
- package/src/assertOptionalStringMaxLength.js +11 -0
- package/src/assertPositiveFiniteInteger.js +14 -0
- package/src/assertPositiveFiniteNumber.js +12 -0
- package/src/buildHumanRequestId.js +9 -0
- package/src/cache/nodeDiffCache.js +124 -0
- package/src/ensure.js +18 -0
- package/src/ensureSqlMessageStorage.js +11 -0
- package/src/ensureSqlMessageStorageEffect.js +12 -0
- package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
- package/src/frame-codec/FrameDelta.ts +6 -0
- package/src/frame-codec/FrameDeltaOp.ts +20 -0
- package/src/frame-codec/FrameEncoding.ts +1 -0
- package/src/frame-codec/JsonPath.ts +3 -0
- package/src/frame-codec/JsonPathSegment.ts +1 -0
- package/src/frame-codec/applyFrameDelta.js +143 -0
- package/src/frame-codec/applyFrameDeltaJson.js +10 -0
- package/src/frame-codec/encodeFrameDelta.js +247 -0
- package/src/frame-codec/index.js +15 -0
- package/src/frame-codec/normalizeFrameEncoding.js +13 -0
- package/src/frame-codec/parseFrameDelta.js +27 -0
- package/src/frame-codec/serializeFrameDelta.js +9 -0
- package/src/frame-codec.js +409 -0
- package/src/getSqlMessageStorage.js +11 -0
- package/src/index.d.ts +5203 -0
- package/src/index.js +20 -0
- package/src/input-bounds.js +12 -0
- package/src/input.js +17 -0
- package/src/internal-schema/index.js +19 -0
- package/src/internal-schema/smithersAlerts.js +27 -0
- package/src/internal-schema/smithersApprovals.js +18 -0
- package/src/internal-schema/smithersAttempts.js +20 -0
- package/src/internal-schema/smithersCache.js +13 -0
- package/src/internal-schema/smithersCron.js +11 -0
- package/src/internal-schema/smithersEvents.js +10 -0
- package/src/internal-schema/smithersFrames.js +14 -0
- package/src/internal-schema/smithersHumanRequests.js +17 -0
- package/src/internal-schema/smithersNodeDiffs.js +12 -0
- package/src/internal-schema/smithersNodes.js +13 -0
- package/src/internal-schema/smithersRalph.js +10 -0
- package/src/internal-schema/smithersRuns.js +22 -0
- package/src/internal-schema/smithersSandboxes.js +16 -0
- package/src/internal-schema/smithersSignals.js +12 -0
- package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
- package/src/internal-schema/smithersToolCalls.js +19 -0
- package/src/internal-schema/smithersVectors.js +12 -0
- package/src/internal-schema.js +245 -0
- package/src/isRetryableSqliteWriteError.js +53 -0
- package/src/loadInputEffect.js +28 -0
- package/src/loadOutputsEffect.js +87 -0
- package/src/output/OutputKey.ts +1 -0
- package/src/output/buildKeyWhere.js +17 -0
- package/src/output/buildOutputRow.js +34 -0
- package/src/output/describeSchemaShape.js +70 -0
- package/src/output/getAgentOutputSchema.js +13 -0
- package/src/output/getKeyColumns.js +19 -0
- package/src/output/index.js +14 -0
- package/src/output/selectOutputRowEffect.js +30 -0
- package/src/output/stripAutoColumns.js +10 -0
- package/src/output/upsertOutputRowEffect.js +38 -0
- package/src/output/validateExistingOutput.js +17 -0
- package/src/output/validateOutput.js +17 -0
- package/src/output-schema-descriptor.js +163 -0
- package/src/output.js +240 -0
- package/src/react-output.js +10 -0
- package/src/runState/ComputeRunStateOptions.ts +4 -0
- package/src/runState/DeriveRunStateInput.ts +10 -0
- package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
- package/src/runState/ReasonBlocked.ts +10 -0
- package/src/runState/ReasonUnhealthy.ts +6 -0
- package/src/runState/RunState.ts +12 -0
- package/src/runState/RunStateView.ts +11 -0
- package/src/runState/computeRunState.js +22 -0
- package/src/runState/computeRunStateFromRow.js +102 -0
- package/src/runState/deriveRunState.js +109 -0
- package/src/runState/parseEventMeta.js +18 -0
- package/src/runState/parseTimerMeta.js +16 -0
- package/src/runState-types.ts +23 -0
- package/src/runState.js +7 -0
- package/src/schema-signature.js +22 -0
- package/src/snapshot.js +125 -0
- package/src/sql-message-storage.js +839 -0
- package/src/storage/InMemoryStorage.js +484 -0
- package/src/storage/StorageService.js +7 -0
- package/src/storage/StorageServiceShape.ts +122 -0
- package/src/storage/StorageServiceTypes.ts +150 -0
- package/src/unwrapZodType.js +17 -0
- package/src/utils/camelToSnake.js +6 -0
- package/src/withSqliteWriteRetryEffect.js +110 -0
- package/src/write-retry.js +49 -0
- package/src/zodToCreateTableSQL.js +41 -0
- package/src/zodToTable.js +60 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
import { assertMaxBytes } from "./assertMaxBytes.js";
|
|
3
|
+
import { assertMaxJsonDepth } from "./assertMaxJsonDepth.js";
|
|
4
|
+
/** @typedef {import("./JsonBounds.ts").JsonBounds} JsonBounds */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} field
|
|
8
|
+
* @param {unknown} value
|
|
9
|
+
* @param {JsonBounds} bounds
|
|
10
|
+
* @param {string} path
|
|
11
|
+
* @param {Set<unknown>} seen
|
|
12
|
+
*/
|
|
13
|
+
function validateJsonValue(field, value, bounds, path, seen) {
|
|
14
|
+
if (value === null || typeof value === "boolean") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
if (typeof bounds.maxStringLength === "number" &&
|
|
19
|
+
value.length > bounds.maxStringLength) {
|
|
20
|
+
throw new SmithersError("INVALID_INPUT", `${field} contains a string exceeding ${bounds.maxStringLength} characters.`, {
|
|
21
|
+
field,
|
|
22
|
+
path,
|
|
23
|
+
maxLength: bounds.maxStringLength,
|
|
24
|
+
actualLength: value.length,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
if (!Number.isFinite(value)) {
|
|
31
|
+
throw new SmithersError("INVALID_INPUT", `${field} must contain only finite numbers.`, { field, path, value });
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (value === undefined ||
|
|
36
|
+
typeof value === "bigint" ||
|
|
37
|
+
typeof value === "function" ||
|
|
38
|
+
typeof value === "symbol") {
|
|
39
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be JSON-serializable.`, { field, path, valueType: typeof value });
|
|
40
|
+
}
|
|
41
|
+
if (typeof value !== "object") {
|
|
42
|
+
throw new SmithersError("INVALID_INPUT", `${field} contains an unsupported value.`, { field, path, valueType: typeof value });
|
|
43
|
+
}
|
|
44
|
+
if (seen.has(value)) {
|
|
45
|
+
throw new SmithersError("INVALID_INPUT", `${field} must not contain circular references.`, { field, path });
|
|
46
|
+
}
|
|
47
|
+
seen.add(value);
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
if (typeof bounds.maxArrayLength === "number" &&
|
|
50
|
+
value.length > bounds.maxArrayLength) {
|
|
51
|
+
throw new SmithersError("INVALID_INPUT", `${field} contains an array exceeding ${bounds.maxArrayLength} items.`, {
|
|
52
|
+
field,
|
|
53
|
+
path,
|
|
54
|
+
maxLength: bounds.maxArrayLength,
|
|
55
|
+
actualLength: value.length,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
59
|
+
validateJsonValue(field, value[index], bounds, `${path}[${index}]`, seen);
|
|
60
|
+
}
|
|
61
|
+
seen.delete(value);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
65
|
+
validateJsonValue(field, entry, bounds, `${path}.${key}`, seen);
|
|
66
|
+
}
|
|
67
|
+
seen.delete(value);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} field
|
|
71
|
+
* @param {unknown} value
|
|
72
|
+
* @param {JsonBounds} bounds
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function assertJsonPayloadWithinBounds(field, value, bounds) {
|
|
76
|
+
let payloadJson;
|
|
77
|
+
try {
|
|
78
|
+
payloadJson = JSON.stringify(value);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be JSON-serializable.`, { field }, { cause: error });
|
|
82
|
+
}
|
|
83
|
+
if (payloadJson === undefined) {
|
|
84
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be JSON-serializable.`, { field });
|
|
85
|
+
}
|
|
86
|
+
if (typeof bounds.maxBytes === "number") {
|
|
87
|
+
assertMaxBytes(field, payloadJson, bounds.maxBytes);
|
|
88
|
+
}
|
|
89
|
+
if (typeof bounds.maxDepth === "number") {
|
|
90
|
+
assertMaxJsonDepth(field, value, bounds.maxDepth);
|
|
91
|
+
}
|
|
92
|
+
validateJsonValue(field, value, bounds, field, new Set());
|
|
93
|
+
return payloadJson;
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} field
|
|
4
|
+
* @param {string | ArrayBuffer | ArrayBufferView} value
|
|
5
|
+
* @param {number} maxBytes
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
export function assertMaxBytes(field, value, maxBytes) {
|
|
9
|
+
let actualBytes;
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
actualBytes = Buffer.byteLength(value, "utf8");
|
|
12
|
+
}
|
|
13
|
+
else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
14
|
+
actualBytes = value.byteLength;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be a string or byte buffer.`, { field, valueType: typeof value });
|
|
18
|
+
}
|
|
19
|
+
if (actualBytes > maxBytes) {
|
|
20
|
+
throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum size of ${maxBytes} bytes.`, { field, maxBytes, actualBytes });
|
|
21
|
+
}
|
|
22
|
+
return actualBytes;
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} field
|
|
4
|
+
* @param {unknown} value
|
|
5
|
+
* @param {number} depth
|
|
6
|
+
* @param {number} maxDepth
|
|
7
|
+
* @param {string} path
|
|
8
|
+
* @param {Set<unknown>} seen
|
|
9
|
+
*/
|
|
10
|
+
function validateJsonDepth(field, value, depth, maxDepth, path, seen) {
|
|
11
|
+
if (depth > maxDepth) {
|
|
12
|
+
throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum JSON depth of ${maxDepth}.`, { field, maxDepth, path });
|
|
13
|
+
}
|
|
14
|
+
if (value === null || typeof value !== "object") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (seen.has(value)) {
|
|
18
|
+
throw new SmithersError("INVALID_INPUT", `${field} must not contain circular references.`, { field, path });
|
|
19
|
+
}
|
|
20
|
+
seen.add(value);
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
23
|
+
validateJsonDepth(field, value[index], depth + 1, maxDepth, `${path}[${index}]`, seen);
|
|
24
|
+
}
|
|
25
|
+
seen.delete(value);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
29
|
+
validateJsonDepth(field, entry, depth + 1, maxDepth, `${path}.${key}`, seen);
|
|
30
|
+
}
|
|
31
|
+
seen.delete(value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} field
|
|
35
|
+
* @param {unknown} value
|
|
36
|
+
* @param {number} maxDepth
|
|
37
|
+
*/
|
|
38
|
+
export function assertMaxJsonDepth(field, value, maxDepth) {
|
|
39
|
+
validateJsonDepth(field, value, 1, maxDepth, field, new Set());
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} field
|
|
4
|
+
* @param {unknown} value
|
|
5
|
+
* @param {number} maxLength
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function assertMaxStringLength(field, value, maxLength) {
|
|
9
|
+
if (typeof value !== "string") {
|
|
10
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be a string.`, { field, valueType: typeof value });
|
|
11
|
+
}
|
|
12
|
+
if (value.length > maxLength) {
|
|
13
|
+
throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum length of ${maxLength} characters.`, { field, maxLength, actualLength: value.length });
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} field
|
|
4
|
+
* @param {unknown} value
|
|
5
|
+
* @param {number} maxLength
|
|
6
|
+
*/
|
|
7
|
+
export function assertOptionalArrayMaxLength(field, value, maxLength) {
|
|
8
|
+
if (value === undefined || value === null)
|
|
9
|
+
return;
|
|
10
|
+
if (!Array.isArray(value)) {
|
|
11
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be an array.`, { field, valueType: typeof value });
|
|
12
|
+
}
|
|
13
|
+
if (value.length > maxLength) {
|
|
14
|
+
throw new SmithersError("INVALID_INPUT", `${field} exceeds the maximum size of ${maxLength}.`, { field, maxLength, actualLength: value.length });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { assertMaxStringLength } from "./assertMaxStringLength.js";
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} field
|
|
4
|
+
* @param {unknown} value
|
|
5
|
+
* @param {number} maxLength
|
|
6
|
+
*/
|
|
7
|
+
export function assertOptionalStringMaxLength(field, value, maxLength) {
|
|
8
|
+
if (value === undefined || value === null)
|
|
9
|
+
return;
|
|
10
|
+
assertMaxStringLength(field, value, maxLength);
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
import { assertPositiveFiniteNumber } from "./assertPositiveFiniteNumber.js";
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} field
|
|
5
|
+
* @param {unknown} value
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
export function assertPositiveFiniteInteger(field, value) {
|
|
9
|
+
const numberValue = assertPositiveFiniteNumber(field, value);
|
|
10
|
+
if (!Number.isInteger(numberValue)) {
|
|
11
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be an integer greater than 0.`, { field, value });
|
|
12
|
+
}
|
|
13
|
+
return numberValue;
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} field
|
|
4
|
+
* @param {unknown} value
|
|
5
|
+
* @returns {number}
|
|
6
|
+
*/
|
|
7
|
+
export function assertPositiveFiniteNumber(field, value) {
|
|
8
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
9
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be a finite number greater than 0.`, { field, value });
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} _SmithersDb */
|
|
2
|
+
/** @typedef {import("../adapter/NodeDiffCacheRow.ts").NodeDiffCacheRow} _NodeDiffCacheRow */
|
|
3
|
+
/** @typedef {{ bundle: unknown; sizeBytes: number; cacheResult: "hit" | "miss" }} NodeDiffCacheResult */
|
|
4
|
+
|
|
5
|
+
const NODE_DIFF_MAX_BYTES = 50 * 1024 * 1024;
|
|
6
|
+
/** @type {WeakMap<object, Map<string, Promise<NodeDiffCacheResult>>>} */
|
|
7
|
+
const inflightByDb = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} dbKey
|
|
10
|
+
* @returns {Map<string, Promise<NodeDiffCacheResult>>}
|
|
11
|
+
*/
|
|
12
|
+
function getInflightMap(dbKey) {
|
|
13
|
+
const existing = inflightByDb.get(dbKey);
|
|
14
|
+
if (existing) return existing;
|
|
15
|
+
/** @type {Map<string, Promise<NodeDiffCacheResult>>} */
|
|
16
|
+
const created = new Map();
|
|
17
|
+
inflightByDb.set(dbKey, created);
|
|
18
|
+
return created;
|
|
19
|
+
}
|
|
20
|
+
export class NodeDiffTooLargeError extends Error {
|
|
21
|
+
code = "DiffTooLarge";
|
|
22
|
+
/** @type {number} */
|
|
23
|
+
sizeBytes;
|
|
24
|
+
/** @param {number} sizeBytes */
|
|
25
|
+
constructor(sizeBytes) {
|
|
26
|
+
super(`Serialized diff exceeds ${NODE_DIFF_MAX_BYTES} bytes`);
|
|
27
|
+
this.name = "NodeDiffTooLargeError";
|
|
28
|
+
this.sizeBytes = sizeBytes;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class NodeDiffCache {
|
|
32
|
+
/** @type {_SmithersDb} */
|
|
33
|
+
adapter;
|
|
34
|
+
/** @type {{ warn?: (message: string, details?: Record<string, unknown>) => void }} */
|
|
35
|
+
logger;
|
|
36
|
+
/**
|
|
37
|
+
* @param {_SmithersDb} adapter
|
|
38
|
+
* @param {{ warn?: (message: string, details?: Record<string, unknown>) => void }} [logger]
|
|
39
|
+
*/
|
|
40
|
+
constructor(adapter, logger = {}) {
|
|
41
|
+
this.adapter = adapter;
|
|
42
|
+
this.logger = logger;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @param {{ runId: string; nodeId: string; iteration: number; baseRef: string; }} key
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
static keyString(key) {
|
|
49
|
+
return `${key.runId}::${key.nodeId}::${key.iteration}::${key.baseRef}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @param {{ runId: string; nodeId: string; iteration: number; baseRef: string; }} key
|
|
53
|
+
* @returns {Promise<{ bundle: unknown; sizeBytes: number; } | null>}
|
|
54
|
+
*/
|
|
55
|
+
async get(key) {
|
|
56
|
+
const row = await this.adapter.getNodeDiffCache(key.runId, key.nodeId, key.iteration, key.baseRef);
|
|
57
|
+
if (!row || typeof row.diffJson !== "string") return null;
|
|
58
|
+
try {
|
|
59
|
+
const bundle = JSON.parse(row.diffJson);
|
|
60
|
+
const sizeBytes = Number(row.sizeBytes ?? Buffer.byteLength(row.diffJson, "utf8"));
|
|
61
|
+
return {
|
|
62
|
+
bundle,
|
|
63
|
+
sizeBytes: Number.isFinite(sizeBytes) ? sizeBytes : Buffer.byteLength(row.diffJson, "utf8"),
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
this.logger.warn?.("Failed to parse cached node diff JSON; treating as miss.", {
|
|
67
|
+
runId: key.runId, nodeId: key.nodeId, iteration: key.iteration,
|
|
68
|
+
});
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @param {{ runId: string; nodeId: string; iteration: number; baseRef: string; }} key
|
|
74
|
+
* @param {() => Promise<unknown>} compute
|
|
75
|
+
* @returns {Promise<NodeDiffCacheResult>}
|
|
76
|
+
*/
|
|
77
|
+
async getOrCompute(key, compute) {
|
|
78
|
+
const hit = await this.get(key);
|
|
79
|
+
if (hit) return { bundle: hit.bundle, sizeBytes: hit.sizeBytes, cacheResult: "hit" };
|
|
80
|
+
const adapterWithDb = /** @type {_SmithersDb & { db?: object }} */ (this.adapter);
|
|
81
|
+
const inflight = getInflightMap(adapterWithDb.db ?? this.adapter);
|
|
82
|
+
const inflightKey = NodeDiffCache.keyString(key);
|
|
83
|
+
const pending = inflight.get(inflightKey);
|
|
84
|
+
if (pending) return pending;
|
|
85
|
+
const computePromise = (async () => {
|
|
86
|
+
const bundle = await compute();
|
|
87
|
+
const diffJson = JSON.stringify(bundle);
|
|
88
|
+
const sizeBytes = Buffer.byteLength(diffJson, "utf8");
|
|
89
|
+
if (sizeBytes > NODE_DIFF_MAX_BYTES) throw new NodeDiffTooLargeError(sizeBytes);
|
|
90
|
+
/** @type {_NodeDiffCacheRow} */
|
|
91
|
+
const row = {
|
|
92
|
+
runId: key.runId, nodeId: key.nodeId, iteration: key.iteration, baseRef: key.baseRef,
|
|
93
|
+
diffJson, computedAtMs: Date.now(), sizeBytes,
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
await this.adapter.upsertNodeDiffCache(row);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.logger.warn?.("Failed writing node diff cache row.", {
|
|
99
|
+
runId: key.runId, nodeId: key.nodeId, iteration: key.iteration,
|
|
100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { bundle, sizeBytes, cacheResult: /** @type {const} */ ("miss") };
|
|
104
|
+
})().finally(() => { inflight.delete(inflightKey); });
|
|
105
|
+
inflight.set(inflightKey, computePromise);
|
|
106
|
+
return computePromise;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} runId
|
|
110
|
+
* @param {number} targetFrameNo
|
|
111
|
+
* @returns {ReturnType<_SmithersDb["invalidateNodeDiffsAfterFrame"]>}
|
|
112
|
+
*/
|
|
113
|
+
invalidateAfterFrame(runId, targetFrameNo) {
|
|
114
|
+
return this.adapter.invalidateNodeDiffsAfterFrame(runId, targetFrameNo);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} [runId]
|
|
118
|
+
* @returns {ReturnType<_SmithersDb["countNodeDiffCacheRows"]>}
|
|
119
|
+
*/
|
|
120
|
+
countRows(runId) {
|
|
121
|
+
return this.adapter.countNodeDiffCacheRows(runId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export { NODE_DIFF_MAX_BYTES };
|
package/src/ensure.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { ensureSqlMessageStorageEffect } from "./sql-message-storage.js";
|
|
3
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} _BunSQLiteDatabase */
|
|
4
|
+
/** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} _SmithersError */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {_BunSQLiteDatabase<Record<string, unknown>>} db
|
|
8
|
+
* @returns {Effect.Effect<void, _SmithersError>}
|
|
9
|
+
*/
|
|
10
|
+
export function ensureSmithersTablesEffect(db) {
|
|
11
|
+
return ensureSqlMessageStorageEffect(db).pipe(Effect.withLogSpan("db:ensure-smithers-tables"));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @param {_BunSQLiteDatabase<Record<string, unknown>>} db
|
|
15
|
+
*/
|
|
16
|
+
export function ensureSmithersTables(db) {
|
|
17
|
+
Effect.runSync(ensureSmithersTablesEffect(db));
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { getSqlMessageStorage } from "./getSqlMessageStorage.js";
|
|
3
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {BunSQLiteDatabase<any> | Database} db
|
|
7
|
+
* @returns {Promise<void>}
|
|
8
|
+
*/
|
|
9
|
+
export function ensureSqlMessageStorage(db) {
|
|
10
|
+
return getSqlMessageStorage(db).ensureSchema();
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { getSqlMessageStorage } from "./getSqlMessageStorage.js";
|
|
4
|
+
/** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {BunSQLiteDatabase<any> | Database} db
|
|
8
|
+
* @returns {Effect.Effect<void, never>}
|
|
9
|
+
*/
|
|
10
|
+
export function ensureSqlMessageStorageEffect(db) {
|
|
11
|
+
return getSqlMessageStorage(db).ensureSchemaEffect();
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const FRAME_KEYFRAME_INTERVAL = 50;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JsonPath } from "./JsonPath";
|
|
2
|
+
|
|
3
|
+
export type FrameDeltaOp =
|
|
4
|
+
| {
|
|
5
|
+
op: "set";
|
|
6
|
+
path: JsonPath;
|
|
7
|
+
value: unknown;
|
|
8
|
+
nodeId?: string;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
op: "insert";
|
|
12
|
+
path: JsonPath;
|
|
13
|
+
value: unknown;
|
|
14
|
+
nodeId?: string;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
op: "remove";
|
|
18
|
+
path: JsonPath;
|
|
19
|
+
nodeId?: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type FrameEncoding = "full" | "delta" | "keyframe";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type JsonPathSegment = string | number;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { canonicalizeXml, parseXmlJson } from "@smithers-orchestrator/graph/utils/xml";
|
|
2
|
+
/** @typedef {import("./FrameDeltaOp.ts").FrameDeltaOp} FrameDeltaOp */
|
|
3
|
+
/** @typedef {import("./JsonPath.ts").JsonPath} JsonPath */
|
|
4
|
+
/** @typedef {import("./JsonPathSegment.ts").JsonPathSegment} JsonPathSegment */
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("./FrameDelta.ts").FrameDelta} FrameDelta */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} previousXmlJson
|
|
10
|
+
* @param {FrameDelta} delta
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function applyFrameDelta(previousXmlJson, delta) {
|
|
14
|
+
const root = cloneValue(parseXmlJson(previousXmlJson));
|
|
15
|
+
const next = applyOps(root, delta.ops);
|
|
16
|
+
return canonicalizeXml(next);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @param {unknown} root
|
|
20
|
+
* @param {FrameDeltaOp[]} ops
|
|
21
|
+
* @returns {unknown}
|
|
22
|
+
*/
|
|
23
|
+
function applyOps(root, ops) {
|
|
24
|
+
let current = root;
|
|
25
|
+
for (const op of ops) {
|
|
26
|
+
if (op.op === "set") {
|
|
27
|
+
current = setAtPath(current, op.path, op.value);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (op.op === "insert") {
|
|
31
|
+
current = insertAtPath(current, op.path, op.value);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
current = removeAtPath(current, op.path);
|
|
35
|
+
}
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {unknown} root
|
|
40
|
+
* @param {JsonPath} path
|
|
41
|
+
* @param {unknown} value
|
|
42
|
+
* @returns {unknown}
|
|
43
|
+
*/
|
|
44
|
+
function setAtPath(root, path, value) {
|
|
45
|
+
if (path.length === 0) {
|
|
46
|
+
return cloneValue(value);
|
|
47
|
+
}
|
|
48
|
+
const { parent, key } = getParentAndKey(root, path);
|
|
49
|
+
if (Array.isArray(parent)) {
|
|
50
|
+
if (typeof key !== "number") {
|
|
51
|
+
throw new Error("Invalid array set path");
|
|
52
|
+
}
|
|
53
|
+
parent[key] = cloneValue(value);
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
if (!isRecord(parent) || typeof key !== "string") {
|
|
57
|
+
throw new Error("Invalid object set path");
|
|
58
|
+
}
|
|
59
|
+
parent[key] = cloneValue(value);
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @param {unknown} root
|
|
64
|
+
* @param {JsonPath} path
|
|
65
|
+
* @param {unknown} value
|
|
66
|
+
* @returns {unknown}
|
|
67
|
+
*/
|
|
68
|
+
function insertAtPath(root, path, value) {
|
|
69
|
+
if (path.length === 0) {
|
|
70
|
+
return cloneValue(value);
|
|
71
|
+
}
|
|
72
|
+
const { parent, key } = getParentAndKey(root, path);
|
|
73
|
+
if (!Array.isArray(parent) || typeof key !== "number") {
|
|
74
|
+
throw new Error("Invalid insert path");
|
|
75
|
+
}
|
|
76
|
+
parent.splice(key, 0, cloneValue(value));
|
|
77
|
+
return root;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* @param {unknown} root
|
|
81
|
+
* @param {JsonPath} path
|
|
82
|
+
* @returns {unknown}
|
|
83
|
+
*/
|
|
84
|
+
function removeAtPath(root, path) {
|
|
85
|
+
if (path.length === 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const { parent, key } = getParentAndKey(root, path);
|
|
89
|
+
if (Array.isArray(parent)) {
|
|
90
|
+
if (typeof key !== "number") {
|
|
91
|
+
throw new Error("Invalid array remove path");
|
|
92
|
+
}
|
|
93
|
+
parent.splice(key, 1);
|
|
94
|
+
return root;
|
|
95
|
+
}
|
|
96
|
+
if (!isRecord(parent) || typeof key !== "string") {
|
|
97
|
+
throw new Error("Invalid object remove path");
|
|
98
|
+
}
|
|
99
|
+
delete parent[key];
|
|
100
|
+
return root;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* @param {unknown} root
|
|
104
|
+
* @param {JsonPath} path
|
|
105
|
+
* @returns {{ parent: unknown; key: JsonPathSegment }}
|
|
106
|
+
*/
|
|
107
|
+
function getParentAndKey(root, path) {
|
|
108
|
+
let cursor = root;
|
|
109
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
110
|
+
const seg = path[i];
|
|
111
|
+
if (typeof seg === "number") {
|
|
112
|
+
if (!Array.isArray(cursor)) {
|
|
113
|
+
throw new Error("Invalid numeric path segment");
|
|
114
|
+
}
|
|
115
|
+
cursor = cursor[seg];
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!isRecord(cursor)) {
|
|
119
|
+
throw new Error("Invalid object path segment");
|
|
120
|
+
}
|
|
121
|
+
cursor = cursor[seg];
|
|
122
|
+
}
|
|
123
|
+
return { parent: cursor, key: path[path.length - 1] };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* @param {unknown} value
|
|
127
|
+
* @returns {value is Record<string, unknown>}
|
|
128
|
+
*/
|
|
129
|
+
function isRecord(value) {
|
|
130
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* @template T
|
|
134
|
+
* @param {T} value
|
|
135
|
+
* @returns {T}
|
|
136
|
+
*/
|
|
137
|
+
function cloneValue(value) {
|
|
138
|
+
if (value === null)
|
|
139
|
+
return value;
|
|
140
|
+
if (typeof value !== "object")
|
|
141
|
+
return value;
|
|
142
|
+
return JSON.parse(JSON.stringify(value));
|
|
143
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { applyFrameDelta } from "./applyFrameDelta.js";
|
|
2
|
+
import { parseFrameDelta } from "./parseFrameDelta.js";
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} previousXmlJson
|
|
5
|
+
* @param {string} deltaJson
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function applyFrameDeltaJson(previousXmlJson, deltaJson) {
|
|
9
|
+
return applyFrameDelta(previousXmlJson, parseFrameDelta(deltaJson));
|
|
10
|
+
}
|